-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* fix name * navigation * update handle * link and fix
- Loading branch information
Din
authored
Dec 16, 2023
1 parent
e7708c2
commit 0351cc4
Showing
16 changed files
with
362 additions
and
53 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import { Organizations } from '@api/database/schema' | ||
import { authProcedure, organizationMemberMiddleware } from '@api/trpc' | ||
import { TRPCError } from '@trpc/server' | ||
import { eq } from 'drizzle-orm' | ||
import { Buffer } from 'node:buffer' | ||
import { z } from 'zod' | ||
|
||
export const organizationChangeLogoRoute = authProcedure | ||
.input( | ||
z.object({ | ||
organization: z.object({ | ||
id: z.string().uuid(), | ||
avatar: z.object({ | ||
name: z | ||
.string() | ||
.max(1234) | ||
.transform((name) => name.replace(/[^a-zA-Z0-9.-_]/gi, '-')), | ||
base64: z | ||
.string() | ||
.max(1024 * 1024) // 1 MB | ||
.transform((base64) => { | ||
return Buffer.from(base64, 'base64') | ||
}), | ||
}), | ||
}), | ||
}), | ||
) | ||
.use(organizationMemberMiddleware) | ||
.mutation(async ({ ctx, input }) => { | ||
const organization = await ctx.db.query.Organizations.findFirst({ | ||
where(t, { eq }) { | ||
return eq(t.id, input.organization.id) | ||
}, | ||
}) | ||
|
||
if (!organization) { | ||
throw new TRPCError({ | ||
code: 'INTERNAL_SERVER_ERROR', | ||
message: 'Organization not found', | ||
}) | ||
} | ||
|
||
const objectName = `organization/${input.organization.id}/logo/${input.organization.avatar.name}` | ||
const deleteOldLogo = ctx.env.PUBLIC_BUCKET.delete(organization.logoUrl) | ||
const uploadNewLogo = ctx.env.PUBLIC_BUCKET.put(objectName, input.organization.avatar.base64) | ||
const updateLogoUrl = ctx.db | ||
.update(Organizations) | ||
.set({ | ||
logoUrl: objectName, | ||
}) | ||
.where(eq(Organizations.id, input.organization.id)) | ||
|
||
await Promise.all([deleteOldLogo, uploadNewLogo, updateLogoUrl]) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,14 @@ | ||
import { router } from '@api/trpc' | ||
import { organizationChangeLogoRoute } from './change-logo' | ||
import { organizationCreateRoute } from './create' | ||
import { organizationDetailRoute } from './detail' | ||
import { organizationListRoute } from './list' | ||
import { organizationUpdateRoute } from './update' | ||
|
||
export const organizationRouter = router({ | ||
list: organizationListRoute, | ||
detail: organizationDetailRoute, | ||
create: organizationCreateRoute, | ||
update: organizationUpdateRoute, | ||
changeLogo: organizationChangeLogoRoute, | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { Organizations } from '@api/database/schema' | ||
import { authProcedure, organizationMemberMiddleware } from '@api/trpc' | ||
import { eq } from 'drizzle-orm' | ||
import { z } from 'zod' | ||
|
||
export const organizationUpdateRoute = authProcedure | ||
.input( | ||
z.object({ | ||
organization: z.object({ | ||
id: z.string().uuid(), | ||
name: z.string(), | ||
}), | ||
}), | ||
) | ||
.use(organizationMemberMiddleware) | ||
.mutation(async ({ ctx, input }) => { | ||
await ctx.db | ||
.update(Organizations) | ||
.set({ | ||
name: input.organization.name, | ||
}) | ||
.where(eq(Organizations.id, input.organization.id)) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -80,4 +80,8 @@ | |
table-layout: fixed; | ||
width: 100%; | ||
} | ||
|
||
img { | ||
@apply object-center object-cover; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
'use client' | ||
|
||
import { api } from '@web/lib/api' | ||
import { isActivePathname } from '@web/lib/utils' | ||
import Link from 'next/link' | ||
import { usePathname, useSearchParams } from 'next/navigation' | ||
import { Skeleton } from '@ui/ui/skeleton' | ||
import { ViewportBlock } from '@ui/ui/viewport-block' | ||
|
||
const staticLinks = [ | ||
{ | ||
name: 'Profile', | ||
href: '/profile', | ||
}, | ||
{ | ||
name: 'Notifications', | ||
href: '/notifications', | ||
}, | ||
] | ||
|
||
export function Nav() { | ||
const pathname = usePathname() | ||
const searchParams = useSearchParams() | ||
const query = api.organization.list.useInfiniteQuery( | ||
{ | ||
limit: 6, | ||
}, | ||
{ | ||
getNextPageParam: (lastPage) => lastPage.nextCursor, | ||
}, | ||
) | ||
|
||
const organizationLinks = | ||
query.data?.pages.flatMap((page) => | ||
page.items.map((organization) => ({ | ||
name: organization.name, | ||
id: organization.id, | ||
href: `organization?id=${organization.id}`, | ||
})), | ||
) ?? [] | ||
|
||
return ( | ||
<nav className="flex py-4"> | ||
<ul | ||
role="list" | ||
className="flex min-w-full flex-none gap-x-6 px-4 text-sm font-semibold leading-6 text-muted-foreground sm:px-6 lg:px-8" | ||
> | ||
{staticLinks.map((link) => ( | ||
<li key={link.name}> | ||
<Link href={link.href} className={isActivePathname(link.href, pathname) ? 'text-primary' : ''}> | ||
{link.name} | ||
</Link> | ||
</li> | ||
))} | ||
|
||
{organizationLinks.map((link) => ( | ||
<li key={link.name}> | ||
<Link | ||
href={link.href} | ||
className={ | ||
isActivePathname(link.href, pathname) && searchParams.get('id') === link.id ? 'text-primary' : '' | ||
} | ||
> | ||
{link.name} | ||
</Link> | ||
</li> | ||
))} | ||
|
||
<li> | ||
{!query.isFetching && query.hasNextPage && <ViewportBlock onEnterViewport={() => query.fetchNextPage()} />} | ||
{(query.hasNextPage || query.isLoading) && <Skeleton className="w-36 h-6" />} | ||
</li> | ||
</ul> | ||
</nav> | ||
) | ||
} |
File renamed without changes.
138 changes: 138 additions & 0 deletions
138
@web/app/(auth)/(settings)/organization/_components/organization-infos-form.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
'use client' | ||
|
||
import { api } from '@web/lib/api' | ||
import { constructPublicResourceUrl } from '@web/lib/utils' | ||
import imageCompression from 'browser-image-compression' | ||
import { Base64 } from 'js-base64' | ||
import { useSearchParams } from 'next/navigation' | ||
import { useId, useRef } from 'react' | ||
import { match } from 'ts-pattern' | ||
import { z } from 'zod' | ||
import { Button } from '@ui/ui/button' | ||
import { GeneralError } from '@ui/ui/general-error' | ||
import { GeneralSkeleton } from '@ui/ui/general-skeleton' | ||
import { Input } from '@ui/ui/input' | ||
import { Label } from '@ui/ui/label' | ||
import { MutationStatusIcon } from '@ui/ui/mutation-status-icon' | ||
|
||
export function OrganizationInfosForm() { | ||
const searchParams = useSearchParams() | ||
const organizationId = z.string().uuid().parse(searchParams.get('id')) | ||
|
||
const nameId = useId() | ||
const query = api.organization.detail.useQuery({ | ||
organizationId, | ||
}) | ||
const mutation = api.organization.update.useMutation() | ||
|
||
const action = (form: FormData) => { | ||
const name = form.get('name') as string | ||
|
||
mutation.mutate({ | ||
organization: { | ||
id: organizationId, | ||
name, | ||
}, | ||
}) | ||
} | ||
|
||
return ( | ||
<div className="@container"> | ||
<section className="grid grid-cols-1 gap-x-8 gap-y-10 px-4 py-16 @2xl:grid-cols-3"> | ||
<div> | ||
<h2 className="font-semibold leading-7">Organization Information</h2> | ||
<p className="mt-1 text-sm leading-6 text-muted-foreground">Edit information about your organization.</p> | ||
</div> | ||
|
||
<div className="@2xl:col-span-2 max-w-xl"> | ||
{match(query) | ||
.with({ status: 'loading' }, () => <GeneralSkeleton count={5} />) | ||
.with({ status: 'error' }, () => <GeneralError />) | ||
.with({ status: 'success' }, (query) => ( | ||
<form action={action}> | ||
<div className="space-y-8"> | ||
<div className="flex items-center gap-8"> | ||
<img | ||
src={constructPublicResourceUrl(query.data.organization.logoUrl)} | ||
alt={query.data.organization.name} | ||
className="h-24 w-24 flex-none rounded-lg bg-background object-cover" | ||
/> | ||
<div> | ||
<LogoChangeButton /> | ||
<p className="mt-2 text-xs leading-5 text-muted-foreground">JPG, GIF or PNG. 1MB max.</p> | ||
</div> | ||
</div> | ||
|
||
<div> | ||
<Label htmlFor={nameId}>Name</Label> | ||
<div className="mt-2"> | ||
<Input | ||
id={nameId} | ||
key={query.data.organization.id} | ||
name="name" | ||
defaultValue={query.data.organization.name} | ||
/> | ||
</div> | ||
</div> | ||
</div> | ||
|
||
<div className="mt-8"> | ||
<Button disabled={mutation.isLoading} className="gap-2"> | ||
Save | ||
<MutationStatusIcon status={mutation.status} /> | ||
</Button> | ||
</div> | ||
</form> | ||
)) | ||
.exhaustive()} | ||
</div> | ||
</section> | ||
</div> | ||
) | ||
} | ||
|
||
export function LogoChangeButton() { | ||
const searchParams = useSearchParams() | ||
const organizationId = z.string().uuid().parse(searchParams.get('id')) | ||
const inputRef = useRef<HTMLInputElement>(null) | ||
const mutation = api.organization.changeLogo.useMutation() | ||
|
||
const onChange = async (event: React.ChangeEvent<HTMLInputElement>) => { | ||
const file = event.target.files?.[0] | ||
if (!file) return | ||
|
||
const compressedImage = await imageCompression(file, { | ||
maxSizeMB: 1, | ||
maxWidthOrHeight: 200, | ||
useWebWorker: true, | ||
}) | ||
|
||
const avatarBase64 = Base64.fromUint8Array(new Uint8Array(await compressedImage.arrayBuffer())) | ||
mutation.mutate({ | ||
organization: { | ||
id: organizationId, | ||
avatar: { | ||
name: compressedImage.name, | ||
base64: avatarBase64, | ||
}, | ||
}, | ||
}) | ||
} | ||
|
||
return ( | ||
<> | ||
<Button | ||
type="button" | ||
className="gap-2" | ||
disabled={mutation.isLoading} | ||
onClick={() => { | ||
inputRef.current?.click() | ||
}} | ||
> | ||
Change logo | ||
<MutationStatusIcon status={mutation.status} /> | ||
</Button> | ||
<input ref={inputRef} type="file" className="hidden" accept="image/*" onChange={onChange} /> | ||
</> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import type { Metadata } from 'next' | ||
import { OrganizationInfosForm } from './_components/organization-infos-form' | ||
|
||
export const metadata: Metadata = { | ||
title: 'Organization', | ||
} | ||
|
||
export default function OrganizationPage() { | ||
return ( | ||
<main className="p-4"> | ||
<div className="mx-auto max-w-7xl divide-y"> | ||
<OrganizationInfosForm /> | ||
</div> | ||
</main> | ||
) | ||
} |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
Oops, something went wrong.