Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 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
0afd581
Update TeamName component to improve input styling and line height
sarimrmalik Apr 20, 2026
53d9569
Merge remote-tracking branch 'origin/main' into refactor/team-settings
sarimrmalik Apr 20, 2026
dd22e48
refactor: remove team-actions and integrate team creation into TRPC
sarimrmalik Apr 20, 2026
1376711
Merge origin/main into refactor/team-settings
sarimrmalik Apr 28, 2026
e4d1cf6
Refactor TeamAvatar and TeamName components to utilize IconButton for…
sarimrmalik Apr 28, 2026
2468dfc
Refactor MembersPage to replace MemberCard with MembersPageContent fo…
sarimrmalik Apr 29, 2026
43b4db3
Enhance TeamNameSchema and TeamName component by introducing a consta…
sarimrmalik Apr 29, 2026
edeca1e
Refactor team creation logic by moving it from TeamsRepository to Use…
sarimrmalik Apr 29, 2026
5efe3e4
Remove unnecessary router.refresh() call in CreateTeamDialog to strea…
sarimrmalik Apr 29, 2026
3bded0f
Refactor TeamAvatar component to accept classNames prop for improved …
sarimrmalik Apr 29, 2026
4466309
Add loader component with CSS animations
sarimrmalik Apr 29, 2026
175d22f
Run biome format
sarimrmalik Apr 29, 2026
4d70596
Merge remote-tracking branch 'origin/main' into refactor/team-settings
sarimrmalik Apr 29, 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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ next-env.d.ts

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


Expand Down
57 changes: 0 additions & 57 deletions src/__test__/unit/teams-repository.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { describe, expect, it, vi } from 'vitest'
import type { components as DashboardComponents } from '@/contracts/dashboard-api'
import { createTeamsRepository } from '@/core/modules/teams/teams-repository.server'

vi.mock('@/core/shared/clients/supabase/admin', () => ({
Expand All @@ -12,63 +11,7 @@ vi.mock('@/core/shared/clients/supabase/admin', () => ({
},
}))

function createApiResponse<T>(input: {
ok: boolean
status: number
data?: T
error?: { message?: string } | null
}) {
return {
data: input.data,
error: input.error ?? null,
response: {
ok: input.ok,
status: input.status,
},
}
}

describe('createTeamsRepository', () => {
it('returns a validation repo error when createTeam gets a 400 response', async () => {
const apiClient = {
POST: vi.fn().mockResolvedValue(
createApiResponse<
DashboardComponents['schemas']['TeamResolveResponse']
>({
ok: false,
status: 400,
error: { message: 'Team name is invalid' },
})
),
GET: vi.fn(),
PATCH: vi.fn(),
DELETE: vi.fn(),
}

const repository = createTeamsRepository(
{ accessToken: 'token' },
{
apiClient:
apiClient as unknown as typeof import('@/core/shared/clients/api').api,
authHeaders: vi.fn(() => ({ 'X-Supabase-Token': 'token' })),
adminClient: {
auth: { admin: { getUserById: vi.fn() } },
} as unknown as typeof import('@/core/shared/clients/supabase/admin').supabaseAdmin,
}
)

const result = await repository.createTeam('bad name')

expect(result).toEqual({
ok: false,
error: expect.objectContaining({
code: 'validation',
status: 400,
message: 'Team name is invalid',
}),
})
})

it('returns a repo error instead of throwing when a team-scoped method has no teamId', async () => {
const repository = createTeamsRepository(
{ accessToken: 'token' },
Expand Down
58 changes: 58 additions & 0 deletions src/__test__/unit/user-teams-repository.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { describe, expect, it, vi } from 'vitest'
import type { components as DashboardComponents } from '@/contracts/dashboard-api'
import { createUserTeamsRepository } from '@/core/modules/teams/user-teams-repository.server'

function createApiResponse<T>(input: {
ok: boolean
status: number
data?: T
error?: { message?: string } | null
}) {
return {
data: input.data,
error: input.error ?? null,
response: {
ok: input.ok,
status: input.status,
},
}
}

describe('createUserTeamsRepository', () => {
it('returns a validation repo error when createTeam gets a 400 response', async () => {
const apiClient = {
POST: vi.fn().mockResolvedValue(
createApiResponse<
DashboardComponents['schemas']['TeamResolveResponse']
>({
ok: false,
status: 400,
error: { message: 'Team name is invalid' },
})
),
GET: vi.fn(),
PATCH: vi.fn(),
DELETE: vi.fn(),
}

const repository = createUserTeamsRepository(
{ accessToken: 'token' },
{
apiClient:
apiClient as unknown as typeof import('@/core/shared/clients/api').api,
authHeaders: vi.fn(() => ({ 'X-Supabase-Token': 'token' })),
}
)

const result = await repository.createTeam('bad name')

expect(result).toEqual({
ok: false,
error: expect.objectContaining({
code: 'validation',
status: 400,
message: 'Team name is invalid',
}),
})
})
})
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>
)
}
15 changes: 11 additions & 4 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 { MembersPageContent } from '@/features/dashboard/members/members-page-content'
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>
<MembersPageContent />
</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
30 changes: 25 additions & 5 deletions src/core/modules/teams/schemas.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,43 @@
import { z } from 'zod'
import { TeamSlugSchema } from '@/core/shared/schemas/team'

export { TeamSlugSchema }
const TEAM_NAME_MAX_LENGTH = 32

export const TeamNameSchema = z
const TeamNameSchema = z
.string()
.trim()
.min(1, { message: 'Team name cannot be empty' })
.max(32, { message: 'Team name cannot be longer than 32 characters' })
.max(TEAM_NAME_MAX_LENGTH, {
message: `Team name cannot be longer than ${TEAM_NAME_MAX_LENGTH} characters`,
})
.regex(/^[a-zA-Z0-9]+(?:[ _.-][a-zA-Z0-9]+)*$/, {
message:
'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,
TEAM_NAME_MAX_LENGTH,
TeamNameSchema,
TeamSlugSchema,
UpdateTeamNameSchema,
}
38 changes: 1 addition & 37 deletions src/core/modules/teams/teams-repository.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,14 @@ export type TeamsRequestScope = RequestScope & {
}

export interface TeamsRepository {
createTeam(
name: string
): Promise<RepoResult<DashboardComponents['schemas']['TeamResolveResponse']>>
listTeamMembers(): Promise<RepoResult<TeamMember[]>>
updateTeamName(
name: string
): Promise<RepoResult<DashboardComponents['schemas']['UpdateTeamResponse']>>
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 Expand Up @@ -73,39 +70,6 @@ export function createTeamsRepository(
}
): TeamsRepository {
return {
async createTeam(
name
): Promise<
RepoResult<DashboardComponents['schemas']['TeamResolveResponse']>
> {
const { data, error, response } = await deps.apiClient.POST('/teams', {
headers: deps.authHeaders(scope.accessToken),
body: { name },
})

if (!response.ok || error || !data) {
if (response.status === 400) {
return err(
createRepoError({
code: 'validation',
status: response.status,
message: error?.message ?? 'Failed to create team',
cause: error,
})
)
}

return err(
repoErrorFromHttp(
response.status,
error?.message ?? 'Failed to create team',
error
)
)
}

return ok(data)
},
async listTeamMembers(): Promise<RepoResult<TeamMember[]>> {
const teamId = requireTeamId(scope)
if (!teamId.ok) {
Expand Down
41 changes: 37 additions & 4 deletions src/core/modules/teams/user-teams-repository.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import 'server-only'

import { secondsInMinute } from 'date-fns/constants'
import { SUPABASE_AUTH_HEADERS } from '@/configs/api'
import type { components as DashboardComponents } from '@/contracts/dashboard-api'
import { api } from '@/core/shared/clients/api'
import { repoErrorFromHttp } from '@/core/shared/errors'
import { createRepoError, repoErrorFromHttp } from '@/core/shared/errors'
import type { RequestScope } from '@/core/shared/repository-scope'
import { err, ok, type RepoResult } from '@/core/shared/result'
import type { ResolvedTeam, TeamModel } from './models'
Expand All @@ -17,6 +18,9 @@ export type UserTeamsRequestScope = RequestScope

export interface UserTeamsRepository {
listUserTeams(): Promise<RepoResult<TeamModel[]>>
createTeam(
name: string
): Promise<RepoResult<DashboardComponents['schemas']['TeamResolveResponse']>>
resolveTeamBySlug(
slug: string,
next?: { tags?: string[] }
Expand Down Expand Up @@ -52,11 +56,40 @@ export function createUserTeamsRepository(
async listUserTeams(): Promise<RepoResult<TeamModel[]>> {
const teamsResult = await listApiUserTeams()

if (!teamsResult.ok) {
return teamsResult
return teamsResult
},
async createTeam(
name
): Promise<
RepoResult<DashboardComponents['schemas']['TeamResolveResponse']>
> {
const { data, error, response } = await deps.apiClient.POST('/teams', {
headers: deps.authHeaders(scope.accessToken),
body: { name },
})

if (!response.ok || error || !data) {
if (response.status === 400) {
return err(
createRepoError({
code: 'validation',
status: response.status,
message: error?.message ?? 'Failed to create team',
cause: error,
})
)
}

return err(
repoErrorFromHttp(
response.status,
error?.message ?? 'Failed to create team',
error
)
)
}

return ok(teamsResult.data)
return ok(data)
},
async resolveTeamBySlug(
slug: string,
Expand Down
Loading
Loading