diff --git a/apps/platform/trpc/routers/orgRouter/setup/profileRouter.ts b/apps/platform/trpc/routers/orgRouter/setup/profileRouter.ts index 282c1326..afb8a431 100644 --- a/apps/platform/trpc/routers/orgRouter/setup/profileRouter.ts +++ b/apps/platform/trpc/routers/orgRouter/setup/profileRouter.ts @@ -48,7 +48,14 @@ export const orgProfileRouter = router({ setOrgProfile: orgProcedure .input( z.object({ - orgName: z.string().min(3).max(32) + orgName: z.string().min(3).max(32), + orgShortCodeNew: z + .string() + .min(5) + .max(64) + .regex(/^[a-z0-9]*$/, { + message: 'Only lowercase letters and numbers' + }) }) ) .mutation(async ({ ctx, input }) => { @@ -60,7 +67,7 @@ export const orgProfileRouter = router({ } const { db, org } = ctx; const orgId = org?.id; - const { orgName } = input; + const { orgName, orgShortCodeNew } = input; const isAdmin = await isAccountAdminOfOrg(org); if (!isAdmin) { @@ -73,7 +80,8 @@ export const orgProfileRouter = router({ await db .update(orgs) .set({ - name: orgName + name: orgName, + shortcode: orgShortCodeNew }) .where(eq(orgs.id, orgId)); diff --git a/apps/web/src/app/[orgShortCode]/settings/org/page.tsx b/apps/web/src/app/[orgShortCode]/settings/org/page.tsx index a10cf6dc..272b9dff 100644 --- a/apps/web/src/app/[orgShortCode]/settings/org/page.tsx +++ b/apps/web/src/app/[orgShortCode]/settings/org/page.tsx @@ -1,7 +1,7 @@ 'use client'; import { useRouter } from 'next/navigation'; -import { useState, useMemo } from 'react'; +import { useState, useMemo, useEffect } from 'react'; import { Button } from '@/src/components/shadcn-ui/button'; import { Camera, FloppyDisk } from '@phosphor-icons/react'; import { api } from '@/src/lib/trpc'; @@ -13,6 +13,9 @@ import { AvatarModal } from '@/src/components/shared/avatar-modal'; import { PageTitle } from '../_components/page-title'; import { Skeleton } from '@/src/components/shadcn-ui/skeleton'; import { Input } from '@/src/components/shadcn-ui/input'; +import { useDebounce } from '@uidotdev/usehooks'; +import { z } from 'zod'; +import { Check, Plus } from '@phosphor-icons/react'; export default function ProfileComponent() { const router = useRouter(); @@ -26,6 +29,45 @@ export default function ProfileComponent() { }); const [orgNameValue, setOrgNameValue] = useState(currentOrg.name); + const [orgShortCodeValue, setOrgShortCodeValue] = useState( + currentOrg.shortCode + ); + + const debouncedOrgShortCode = useDebounce(orgShortCodeValue, 500); + const checkOrgShortCodeApi = + api.useUtils().org.crud.checkShortCodeAvailability; + + const { + loading: orgShortCodeDataLoading, + data: orgShortCodeData, + error: orgShortCodeError, + run: checkOrgShortCode + } = useLoading(async (signal) => { + if (!debouncedOrgShortCode) return; + //set to initial state + if (debouncedOrgShortCode === orgShortCode) { + return null; + } + const parsed = z + .string() + .min(5) + .max(64) + .regex(/^[a-z0-9]*$/, { + message: 'Only lowercase letters and numbers' + }) + .safeParse(debouncedOrgShortCode); + + if (!parsed.success) { + return { + error: parsed.error.issues[0]?.message ?? null, + available: null + }; + } + return await checkOrgShortCodeApi.fetch( + { shortcode: debouncedOrgShortCode }, + { signal } + ); + }); const avatarUrl = useMemo(() => { return generateAvatarUrl({ @@ -56,15 +98,25 @@ export default function ProfileComponent() { const { loading: saveLoading, run: saveOrgProfile } = useLoading(async () => { await updateOrgProfileApi.mutateAsync({ orgName: orgNameValue, - orgShortCode + orgShortCode, + orgShortCodeNew: orgShortCodeValue + }); + updateOrg(orgShortCode, { + name: orgNameValue, + shortCode: orgShortCodeValue }); - updateOrg(orgShortCode, { name: orgNameValue }); }); if (!adminLoading && !isAdmin) { router.push(`/${orgShortCode}/settings`); } + useEffect(() => { + if (!debouncedOrgShortCode) return; + checkOrgShortCode({ clearData: true, clearError: true }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [debouncedOrgShortCode]); + return (
@@ -107,8 +159,62 @@ export default function ProfileComponent() {
+
+ +
+ + {!orgShortCodeData && orgShortCodeDataLoading && ( +
+ Checking... +
+ )} + + {orgShortCodeData && !orgShortCodeDataLoading && ( +
+ {orgShortCodeData.available ? ( + + ) : ( + + )} + +
+ {orgShortCodeData.available + ? 'Looks good!' + : orgShortCodeData.error} +
+
+ )} + + {orgShortCodeError && !orgShortCodeDataLoading && ( +
+ {orgShortCodeError.message} +
+ )} +