Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,5 @@ yarn-error.log*

# idea files
.idea

certificates
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@better-auth/passkey": "^1.4.17",
"@hookform/resolvers": "^3.9.1",
"@polinetwork/backend": "^0.14.0",
"@polinetwork/backend": "file:../backend/package/dist/",
"@radix-ui/react-alert-dialog": "^1.1.3",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-collapsible": "^1.1.1",
Expand All @@ -25,7 +26,7 @@
"@radix-ui/react-progress": "^1.1.3",
"@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.4",
"@t3-oss/env-nextjs": "^0.13.10",
"@tanstack/react-query": "^5.90.19",
Expand All @@ -34,8 +35,8 @@
"@trpc/next": "11.5.1",
"@trpc/react-query": "11.5.1",
"@trpc/tanstack-react-query": "11.5.1",
"better-auth": "^1.4.15",
"babel-plugin-react-compiler": "1.0.0",
"better-auth": "^1.4.17",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
Expand All @@ -47,6 +48,7 @@
"postgres": "^3.4.4",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-error-boundary": "^6.1.0",
"react-hook-form": "^7.55.0",
"server-only": "^0.0.1",
"sonner": "^2.0.3",
Expand Down
292 changes: 266 additions & 26 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

36 changes: 33 additions & 3 deletions src/app/dashboard/(active)/account/page.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
import { CircleAlert, UserIcon } from "lucide-react"
import { Calendar, CircleAlert, KeyIcon, UserIcon } from "lucide-react"
import { headers } from "next/headers"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { auth } from "@/lib/auth"
import { getInitials } from "@/lib/utils"
import { getServerSession } from "@/server/auth"
import { NewPasskeyButton } from "./passkey-button"
import { SetName } from "./set-name"
import { Telegram } from "./telegram"

export default async function Account() {
const { data: session } = await getServerSession()
if (!session) return

const { data: passkeys, error } = await auth.passkey.listUserPasskeys({
fetchOptions: {
headers: await headers(),
},
})

console.log(passkeys, error)

const { user } = session

return (
<main className="container mx-auto px-4 py-8">
<h2 className="text-accent-foreground mb-4 text-3xl font-bold">Account</h2>

<div className="flex gap-4">
<div className="flex gap-4 mb-12">
<Avatar className="h-32 w-32 rounded-lg">
{user.image && <AvatarImage src={user.image} alt={`propic of ${user.name}`} />}
<AvatarFallback className="rounded-lg text-3xl">
Expand All @@ -40,6 +51,25 @@ export default async function Account() {
</div>
</div>
</div>
<div className="flex flex-col gap-4 justify-start items-start">
<h3>Passkeys</h3>
{passkeys?.map((p) => (
<div className="grid grid-cols-[auto_1fr_auto] w-full gap-4 items-center" key={p.id}>
<div className="bg-primary/30 h-full aspect-square flex justify-center items-center rounded-lg">
<KeyIcon size={16} />
</div>
<div>
<p>{p.name}</p>
<p className="text-muted-foreground text-xs flex justify-start items-center gap-1">
<Calendar size={12} />
Created on {p.createdAt.toLocaleDateString()}
</p>
</div>
<Button variant="destructive">Delete</Button>
</div>
))}
<NewPasskeyButton />
</div>
</main>
)
}
35 changes: 35 additions & 0 deletions src/app/dashboard/(active)/account/passkey-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"use client"

import { useRouter } from "next/navigation"
import { useState } from "react"
import { toast } from "sonner"
import { Button } from "@/components/ui/button"
import { auth } from "@/lib/auth"

export function NewPasskeyButton() {
const router = useRouter()
const [isLoading, setIsLoading] = useState(false)

return (
<Button
disabled={isLoading}
onClick={async () => {
setIsLoading(true)
const { data, error } = await auth.passkey.addPasskey({ name: "default" })
setIsLoading(false)
if (error) {
console.error(error)
toast.error("There was an unexpected error")
return
}

toast.success("Passkey created!")
console.log("passkey created", { data })
router.refresh()
return
}}
>
New Passkey
</Button>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { useRouter } from "next/navigation"
import { useState } from "react"
import { toast } from "sonner"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { useTRPC } from "@/lib/trpc/client"

export function SetAssocNumberDialog({ userId, children }: { userId: string; children?: React.ReactNode }) {
const [value, setValue] = useState<string>("")
const [open, setOpen] = useState<boolean>(false)
const trpc = useTRPC()
const _router = useRouter()

const qc = useQueryClient()
const { mutateAsync, isPending } = useMutation(trpc.azure.members.setAssocNumber.mutationOptions())

function handleOpenChange(v: boolean): void {
setOpen(v)
setValue("")
}

async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
console.log("submit")
e.preventDefault()
if (isPending || !value || Number.isNaN(parseInt(value, 10))) return

const res = await mutateAsync({ userId, assocNumber: parseInt(value, 10) })
handleOpenChange(false)
if (res.error !== null) {
toast.error("There was an error")
console.error(res.error)
return
}

console.log("Updated user assocNumber", userId, value)
await qc.invalidateQueries({ queryKey: trpc.azure.members.getAll.queryKey() })
toast.success(`Updated successfully!`)
}

return (
<Dialog onOpenChange={handleOpenChange} open={open}>
<DialogTrigger asChild>{children ?? <Button variant="outline">Set</Button>}</DialogTrigger>
<DialogContent className="sm:max-w-sm">
<DialogHeader>
<DialogTitle>Set Assoc Number</DialogTitle>
<DialogDescription>This changes the `employeeId` field in the Azure User properties.</DialogDescription>
</DialogHeader>
<form className="grid gap-y-4" onSubmit={handleSubmit}>
<div>
<Label htmlFor="assoc-num">Member Number</Label>
<Input
className="max-w-sm"
type="text"
autoComplete="off"
pattern="\d*"
required
title="Only numbers are allowed"
id="assoc-num"
placeholder="0"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
</div>
<DialogFooter>
<DialogClose asChild>
<Button disabled={isPending} variant="outline">
Cancel
</Button>
</DialogClose>
<Button type="submit" disabled={isPending}>
Save
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}
60 changes: 60 additions & 0 deletions src/app/dashboard/(active)/assoc/columns.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"use client"
import { createColumnHelper, type Row } from "@tanstack/react-table"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import type { ApiOutput } from "@/lib/trpc/types"
import { SetAssocNumberDialog } from "./_components/set-assoc-number-dialog"

type ParsedUser = ApiOutput["azure"]["members"]["getAll"][0]
const ch = createColumnHelper<ParsedUser>()

export const columns = [
ch.accessor("employeeId", {
id: "number",
header: "#",
footer: (props) => props.column.id,
cell: ({ getValue, row }) => {
const value = getValue()
return value ? (
<span>{value}</span>
) : (
<SetAssocNumberDialog userId={row.original.id}>
<Button size="sm" variant="outline">
Set
</Button>
</SetAssocNumberDialog>
)
},
}),
ch.accessor("displayName", {
id: "displayName",
header: "Full Name",
cell: ({ getValue, row }) => (
<>
{row.original.isMember && <Badge className="mr-2">Socio</Badge>}
<span>{getValue()}</span>
</>
),
}),
ch.accessor("mail", { id: "mail", header: "Email" }),
ch.accessor("assignedLicensesIds", {
id: "licenses",
header: "Licenses",
cell: ({ getValue }) => {
const licenses = getValue().sort((a, b) => a.localeCompare(b))
return licenses.map((l) => (
<Badge key={l} className="mr-1" variant={l === "OFFICE_365" ? "default" : "secondary"}>
{l}
</Badge>
))
},
}),
ch.group({
id: "actions",
cell: (props) => <RowActions row={props.row} />,
}),
]

function RowActions({ row }: { row: Row<ParsedUser> }) {
return <div className="flex gap-2 justify-start items-center"></div>
}
19 changes: 19 additions & 0 deletions src/app/dashboard/(active)/assoc/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Suspense } from "react"
import { ErrorBoundary } from "react-error-boundary"
import { Spinner } from "@/components/spinner"
import { getQueryClient, trpc } from "@/lib/trpc/server"
import { AssocTable } from "./table"

export default async function AssocIndex() {
const qc = getQueryClient()
void qc.prefetchQuery(trpc.azure.members.getAll.queryOptions())
return (
<div className="container p-8">
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<Suspense fallback={<Spinner />}>
<AssocTable />
</Suspense>
</ErrorBoundary>
</div>
)
}
54 changes: 54 additions & 0 deletions src/app/dashboard/(active)/assoc/table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"use client"

import { useSuspenseQuery } from "@tanstack/react-query"
import { CreateAssocUser } from "@/components/create-assoc-member"
import { DataTable } from "@/components/data-table"
import { Badge } from "@/components/ui/badge"
import { useTRPC } from "@/lib/trpc/client"
import type { ApiOutput } from "@/lib/trpc/types"
import { columns } from "./columns"

type ParsedUser = ApiOutput["azure"]["members"]["getAll"][0]
function sortByAssocNumber(users: ParsedUser[]) {
if (!users || users.length === 0) return users
if (users.every((u) => !u.employeeId))
return users.sort((a, b) => (a.displayName ?? "").localeCompare(b.displayName ?? ""))

return users.sort((a, b) => {
if (a.employeeId && b.employeeId) {
const aInt = parseInt(a.employeeId, 10)
const bInt = parseInt(b.employeeId, 10)
if (Number.isNaN(aInt) && Number.isNaN(bInt)) return 0
if (Number.isNaN(aInt)) return 1
if (Number.isNaN(bInt)) return -1
return aInt - bInt
}
if (a.employeeId) return -1
if (b.employeeId) return 1
return 0
})
}

export function AssocTable() {
const trpc = useTRPC()
const { data: users } = useSuspenseQuery(trpc.azure.members.getAll.queryOptions())

return (
<div className="space-y-3">
<div className="flex gap-2 items-center">
<p>Utenti MS @polinetwork.org</p>
<Badge>{users.length} Soci</Badge>
<Badge variant="secondary">{users.length} fuori</Badge>
<Badge>{users.filter((u) => u.assignedLicensesIds.includes("OFFICE_365")).length} licenze Office</Badge>
<div className="grow" />
<CreateAssocUser />
</div>

<DataTable
data={sortByAssocNumber(users)}
// @ts-expect-error idk what is going on here
columns={columns}
/>
</div>
)
}
6 changes: 5 additions & 1 deletion src/app/dashboard/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { cookies } from "next/headers"
import { redirect } from "next/navigation"
import { SidebarProvider } from "@/components/ui/sidebar"
import { getQueryClient, trpc } from "@/lib/trpc/server"
Expand All @@ -18,5 +19,8 @@ export default async function AdminLayout({ children }: { children: React.ReactN
if (!roles || roles.length === 0) redirect("/onboarding/no-role")
if (roles.includes("creator")) redirect("/onboarding/unauthorized")

return <SidebarProvider>{children}</SidebarProvider>
const cookieStore = await cookies()
const defaultOpen = cookieStore.get("sidebar:state")?.value === "true"

return <SidebarProvider defaultOpen={defaultOpen}>{children}</SidebarProvider>
}
Loading
Loading