Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
9e9322c
Refactor members dashboard experience.
sarimrmalik Apr 8, 2026
1b2d384
Enhance member management features and UI.
sarimrmalik Apr 8, 2026
fa6f937
Refactor member management UI components.
sarimrmalik Apr 8, 2026
d3f41b8
Refactor member table responsiveness and layout.
sarimrmalik Apr 8, 2026
58dc5a8
Refactor member UI components for consistency and clarity.
sarimrmalik Apr 8, 2026
71af2ca
Run biome format
sarimrmalik Apr 8, 2026
b3a7a33
Refactor MembersPage layout to utilize new Page component.
sarimrmalik Apr 8, 2026
09ac3a8
Refactor Add Member components for clarity and consistency.
sarimrmalik Apr 8, 2026
10453df
Implement date formatting utility and enhance member table components.
sarimrmalik Apr 8, 2026
1755e96
Run biome format
sarimrmalik Apr 8, 2026
096f6a9
Undo uppercase for layout titles
sarimrmalik Apr 9, 2026
0c1f82b
Refactor member removal dialog for improved clarity and structure.
sarimrmalik Apr 9, 2026
0adaed1
Refactor member table utility functions for clarity and consistency.
sarimrmalik Apr 9, 2026
680c26c
Add pluralization utility and update member table components.
sarimrmalik Apr 9, 2026
69020e7
Update comment for consistency with other formatting utils
sarimrmalik Apr 9, 2026
7dde923
Refactor InvoicesEmpty component to use TableEmptyState for improved …
sarimrmalik Apr 9, 2026
78b562f
Run biome format
sarimrmalik Apr 9, 2026
3afb7f0
Update dashboard general page layout and enhance user messages.
sarimrmalik Apr 9, 2026
0fe7c7b
Merge remote-tracking branch 'origin/main' into refactor/team-settings
sarimrmalik Apr 9, 2026
3776c02
Enhance team settings page with new components and functionality
sarimrmalik Apr 9, 2026
2f927ea
Refactor team models and repository to streamline data handling
sarimrmalik Apr 10, 2026
cf7dba8
Refactor file path handling in team actions for improved clarity
sarimrmalik Apr 10, 2026
1ed774c
Remove unused components
sarimrmalik Apr 10, 2026
44e6d70
Refactor team avatar and name handling in dashboard settings
sarimrmalik Apr 10, 2026
e8fc524
Refactor team schemas and actions for improved member management
sarimrmalik Apr 10, 2026
f38e6c3
Refactor team members management and loading states
sarimrmalik Apr 10, 2026
810e676
Enhance TeamName component with dynamic font sizing and improved erro…
sarimrmalik Apr 10, 2026
7aa6858
Refactor GeneralPage layout and remove DangerZone component
sarimrmalik Apr 10, 2026
91edd63
Refactor MemberCard component to simplify props and enhance layout
sarimrmalik Apr 10, 2026
4363fa4
Run biome format
sarimrmalik Apr 10, 2026
8edc72c
Enhance TeamName component with pending state check during submission
sarimrmalik Apr 10, 2026
c238e89
Improve error handling in TeamAvatar component during file upload
sarimrmalik Apr 10, 2026
9b7f9f1
Refactor TeamName component for improved layout and error handling
sarimrmalik Apr 10, 2026
c281ff0
fileSchema → FileSchema for consistency
sarimrmalik Apr 10, 2026
2f11401
Add createdAt property to UserTeam schema and update TeamInfo component
sarimrmalik Apr 13, 2026
9fa2fe6
Refactor TeamAvatar and TeamName components for improved functionalit…
sarimrmalik Apr 15, 2026
936cf0a
Fix spacing and gaps
sarimrmalik Apr 15, 2026
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ next-env.d.ts

# AI agents and related files
CLAUDE.md
.cursor/
.agent


Expand Down
4 changes: 4 additions & 0 deletions spec/openapi.dashboard-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,7 @@ components:
- blockedReason
- isDefault
- limits
- createdAt
properties:
id:
type: string
Expand All @@ -412,6 +413,9 @@ components:
type: boolean
limits:
$ref: "#/components/schemas/UserTeamLimits"
createdAt:
type: string
format: date-time

UserTeamsResponse:
type: object
Expand Down
38 changes: 13 additions & 25 deletions src/app/dashboard/[teamSlug]/general/page.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,17 @@
import { InfoCard } from '@/features/dashboard/settings/general/info-card'
import { NameCard } from '@/features/dashboard/settings/general/name-card'
import { ProfilePictureCard } from '@/features/dashboard/settings/general/profile-picture-card'
import Frame from '@/ui/frame'
import { Page } from '@/features/dashboard/layouts/page'
import { TeamAvatar } from '@/features/dashboard/settings/general/team-avatar'
import { TeamInfo } from '@/features/dashboard/settings/general/team-info'
import { TeamName } from '@/features/dashboard/settings/general/team-name'

interface GeneralPageProps {
params: Promise<{
teamSlug: string
}>
}

export default async function GeneralPage({ params }: GeneralPageProps) {
export default async function GeneralPage() {
return (
<Frame
classNames={{
wrapper: 'w-full max-md:p-0',
frame: 'max-md:border-none',
}}
>
<section className="col-span-full flex-col">
<div className="flex gap-2 border-b md:gap-3">
<ProfilePictureCard className="size-32" />
<NameCard />
</div>
<InfoCard className="flex flex-col justify-between" />
</section>
</Frame>
<Page className="flex gap-6">
<TeamAvatar />
<div className="flex min-w-0 flex-1 flex-col gap-4">
<TeamName />
<div className="border-b" />
<TeamInfo />
</div>
</Page>
)
}
13 changes: 10 additions & 3 deletions src/app/dashboard/[teamSlug]/members/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Page } from '@/features/dashboard/layouts/page'
import { MemberCard } from '@/features/dashboard/members/member-card'
import { HydrateClient, prefetch, trpc } from '@/trpc/server'

interface MembersPageProps {
params: Promise<{
Expand All @@ -8,9 +9,15 @@ interface MembersPageProps {
}

export default async function MembersPage({ params }: MembersPageProps) {
const { teamSlug } = await params

prefetch(trpc.teams.members.queryOptions({ teamSlug }))

return (
<Page>
<MemberCard params={params} />
</Page>
<HydrateClient>
<Page>
<MemberCard />
</Page>
</HydrateClient>
)
}
6 changes: 6 additions & 0 deletions src/configs/user-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ export const USER_MESSAGES = {
failedUpdateLogo: {
message: 'Failed to update logo.',
},
teamLogoRemoved: {
message: 'Your team logo has been removed.',
},
failedRemoveLogo: {
message: 'Failed to remove logo.',
},
emailInUse: {
message: 'E-mail already in use.',
},
Expand Down
25 changes: 20 additions & 5 deletions src/core/modules/teams/schemas.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { z } from 'zod'
import { TeamSlugSchema } from '@/core/shared/schemas/team'

export { TeamSlugSchema }

export const TeamNameSchema = z
const TeamNameSchema = z
.string()
.trim()
.min(1, { message: 'Team name cannot be empty' })
Expand All @@ -13,11 +11,28 @@ export const TeamNameSchema = z
'Names can only contain letters and numbers, separated by spaces, underscores, hyphens, or dots',
})

export const UpdateTeamNameSchema = z.object({
const UpdateTeamNameSchema = z.object({
teamSlug: TeamSlugSchema,
name: TeamNameSchema,
})

export const CreateTeamSchema = z.object({
const CreateTeamSchema = z.object({
name: TeamNameSchema,
})

const AddTeamMemberSchema = z.object({
email: z.email(),
})

const RemoveTeamMemberSchema = z.object({
userId: z.uuid(),
})

export {
AddTeamMemberSchema,
CreateTeamSchema,
RemoveTeamMemberSchema,
TeamNameSchema,
TeamSlugSchema,
UpdateTeamNameSchema,
}
2 changes: 1 addition & 1 deletion src/core/modules/teams/teams-repository.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export interface TeamsRepository {
addTeamMember(email: string): Promise<RepoResult<void>>
removeTeamMember(userId: string): Promise<RepoResult<void>>
updateTeamProfilePictureUrl(
profilePictureUrl: string
profilePictureUrl: string | null
): Promise<RepoResult<DashboardComponents['schemas']['UpdateTeamResponse']>>
}

Expand Down
8 changes: 2 additions & 6 deletions src/core/modules/teams/user-teams-repository.server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'server-only'

import { secondsInDay, secondsInMinute } from 'date-fns/constants'
import { secondsInMinute } from 'date-fns/constants'
import { SUPABASE_AUTH_HEADERS } from '@/configs/api'
import { api } from '@/core/shared/clients/api'
import { repoErrorFromHttp } from '@/core/shared/errors'
Expand Down Expand Up @@ -52,11 +52,7 @@ export function createUserTeamsRepository(
async listUserTeams(): Promise<RepoResult<TeamModel[]>> {
const teamsResult = await listApiUserTeams()

if (!teamsResult.ok) {
return teamsResult
}

return ok(teamsResult.data)
return teamsResult
},
async resolveTeamBySlug(
slug: string,
Expand Down
184 changes: 2 additions & 182 deletions src/core/server/actions/team-actions.ts
Original file line number Diff line number Diff line change
@@ -1,96 +1,13 @@
'use server'

import { fileTypeFromBuffer } from 'file-type'
import { revalidatePath } from 'next/cache'
import { after } from 'next/server'
import { returnValidationErrors } from 'next-safe-action'
import { z } from 'zod'
import { zfd } from 'zod-form-data'
import { SUPABASE_AUTH_HEADERS } from '@/configs/api'
import type { CreateTeamsResponse } from '@/core/modules/billing/models'
import {
CreateTeamSchema,
UpdateTeamNameSchema,
} from '@/core/modules/teams/schemas'
import { createTeamsRepository } from '@/core/modules/teams/teams-repository.server'
import {
authActionClient,
withTeamAuthedRequestRepository,
withTeamSlugResolution,
} from '@/core/server/actions/client'
import { CreateTeamSchema } from '@/core/modules/teams/schemas'
import { authActionClient } from '@/core/server/actions/client'
import {
handleDefaultInfraError,
returnServerError,
} from '@/core/server/actions/utils'
import { toActionErrorFromRepoError } from '@/core/server/adapters/errors'
import { l, serializeErrorForLog } from '@/core/shared/clients/logger/logger'
import { deleteFile, getFiles, uploadFile } from '@/core/shared/clients/storage'
import { TeamSlugSchema } from '@/core/shared/schemas/team'

const withTeamsRepository = withTeamAuthedRequestRepository(
createTeamsRepository,
(teamsRepository) => ({ teamsRepository })
)

export const updateTeamNameAction = authActionClient
.schema(UpdateTeamNameSchema)
.metadata({ actionName: 'updateTeamName' })
.use(withTeamSlugResolution)
.use(withTeamsRepository)
.action(async ({ parsedInput, ctx }) => {
const { name, teamSlug } = parsedInput
const result = await ctx.teamsRepository.updateTeamName(name)

if (!result.ok) {
return toActionErrorFromRepoError(result.error)
}

revalidatePath(`/dashboard/${teamSlug}/general`, 'page')

return result.data
})

const AddTeamMemberSchema = z.object({
teamSlug: TeamSlugSchema,
email: z.email(),
})

export const addTeamMemberAction = authActionClient
.schema(AddTeamMemberSchema)
.metadata({ actionName: 'addTeamMember' })
.use(withTeamSlugResolution)
.use(withTeamsRepository)
.action(async ({ parsedInput, ctx }) => {
const { email, teamSlug } = parsedInput
const result = await ctx.teamsRepository.addTeamMember(email)

if (!result.ok) {
return toActionErrorFromRepoError(result.error)
}

revalidatePath(`/dashboard/${teamSlug}/general`, 'page')
})

const RemoveTeamMemberSchema = z.object({
teamSlug: TeamSlugSchema,
userId: z.uuid(),
})

export const removeTeamMemberAction = authActionClient
.schema(RemoveTeamMemberSchema)
.metadata({ actionName: 'removeTeamMember' })
.use(withTeamSlugResolution)
.use(withTeamsRepository)
.action(async ({ parsedInput, ctx }) => {
const { userId, teamSlug } = parsedInput
const result = await ctx.teamsRepository.removeTeamMember(userId)

if (!result.ok) {
return toActionErrorFromRepoError(result.error)
}

revalidatePath(`/dashboard/${teamSlug}/general`, 'page')
})

export const createTeamAction = authActionClient
.schema(CreateTeamSchema)
Expand Down Expand Up @@ -123,100 +40,3 @@ export const createTeamAction = authActionClient

return data
})

const UploadTeamProfilePictureSchema = zfd.formData(
z.object({
teamSlug: zfd.text(),
image: zfd.file(),
})
)

export const uploadTeamProfilePictureAction = authActionClient
.schema(UploadTeamProfilePictureSchema)
.metadata({ actionName: 'uploadTeamProfilePicture' })
.use(withTeamSlugResolution)
.use(withTeamsRepository)
.action(async ({ parsedInput, ctx }) => {
const { image, teamSlug } = parsedInput
const { teamId, teamsRepository } = ctx

const allowedTypes = ['image/jpeg', 'image/png']

if (!allowedTypes.includes(image.type)) {
return returnValidationErrors(UploadTeamProfilePictureSchema, {
image: { _errors: ['File must be JPG or PNG format'] },
})
}

const MAX_FILE_SIZE = 5 * 1024 * 1024

if (image.size > MAX_FILE_SIZE) {
return returnValidationErrors(UploadTeamProfilePictureSchema, {
image: { _errors: ['File size must be less than 5MB'] },
})
}

const arrayBuffer = await image.arrayBuffer()
const buffer = Buffer.from(arrayBuffer)

const fileType = await fileTypeFromBuffer(buffer)

if (!fileType) {
return returnValidationErrors(UploadTeamProfilePictureSchema, {
image: { _errors: ['Unable to determine file type'] },
})
}

const allowedMimeTypes = ['image/jpeg', 'image/png']
if (!allowedMimeTypes.includes(fileType.mime)) {
return returnValidationErrors(UploadTeamProfilePictureSchema, {
image: {
_errors: [
'Invalid file type. Only JPEG and PNG images are allowed. File appears to be: ' +
fileType.mime,
],
},
})
}

const extension = fileType.ext
const fileName = `${Date.now()}.${extension}`
const storagePath = `teams/${teamId}/${fileName}`

const publicUrl = await uploadFile(buffer, storagePath, fileType.mime)

const result = await teamsRepository.updateTeamProfilePictureUrl(publicUrl)
if (!result.ok) {
return toActionErrorFromRepoError(result.error)
}

after(async () => {
try {
const currentFileName = fileName
const folderPath = `teams/${teamId}`
const files = await getFiles(folderPath)

for (const file of files) {
const filePath = file.name
if (filePath === `${folderPath}/${currentFileName}`) {
continue
}

await deleteFile(filePath)
}
} catch (cleanupError) {
l.warn({
key: 'upload_team_profile_picture_action:cleanup_error',
error: serializeErrorForLog(cleanupError),
team_id: teamId,
context: {
image: image.name,
},
})
}
})

revalidatePath(`/dashboard/${teamSlug}/general`, 'page')

return result.data
})
Loading
Loading