Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
f887bbd
feat: inline editing on review viewer for education
sahilverma-dev Jul 17, 2025
d73d143
feat: new date input component added
sahilverma-dev Jul 20, 2025
f11cc14
feat: date range input component added
sahilverma-dev Jul 20, 2025
fda55fc
feat: date month selector on resume editor added
sahilverma-dev Jul 26, 2025
62abc56
feat: debounce added to inline editing
sahilverma-dev Jul 26, 2025
7d9d14c
LET-105 | comp: Route for utils service to call and render resume pre…
pingSubhajit Jul 15, 2025
f898617
LET 93 | comp: redesign the section tabs in the resume editor and ski…
sourabhrathourr Jul 16, 2025
759aaff
LET-113 | core: Implement a seamless transition between the onboardin…
pingSubhajit Jul 16, 2025
c54578f
LET-114 | bug: Polish the onboarding experience (#157)
pingSubhajit Jul 16, 2025
08050a3
LET-102 | comp: CertificationEditor for ResumeEditor component (#159)
shadevkumar Jul 20, 2025
eff23ef
LET 108 | feat(editor): Add feature flag for new resume editor tabs d…
sourabhrathourr Jul 22, 2025
b74d299
LET-62 | comp: ProjectEditor for ResumeEditor component (#163)
shadevkumar Jul 22, 2025
43e7a3a
LET 36 | core: skills section for default resume theme (#164)
sourabhrathourr Jul 23, 2025
f567d60
LET-64 | logic: Identify users in Sentry from Next.js service through…
pingSubhajit Jul 25, 2025
f4014a8
LET-65 | logic: Identify users in Posthog from NextJS service through…
pingSubhajit Jul 26, 2025
24c5c61
LET-90 | comp: Loading indicator for ResumeEditors and ResumeView (#167)
pingSubhajit Jul 26, 2025
9ed417e
LET-118 | core: Refactor skill types to accommodate for the changed s…
pingSubhajit Jul 26, 2025
0d808e7
LET-17 | core: Project section for the default theme (#168)
shadevkumar Jul 26, 2025
0789274
LET-20 | core: Certification section for the default theme (#172)
shadevkumar Jul 27, 2025
8cf3237
LET-79 | core: Configure unit testing for this repository (#158)
pingSubhajit Jul 27, 2025
9c61602
LET-119 | bug: Resume viewer not loading due to parser mismatch of ex…
pingSubhajit Jul 28, 2025
2bd575e
LET 115 | impr: resume editor improvements (#173)
sourabhrathourr Jul 30, 2025
4b2509c
LET-120 | bug: Resume preview route not loading & resume data structu…
pingSubhajit Aug 3, 2025
fc70167
LET-121 | logic: API endpoint to parse user uploaded resume and parse…
pingSubhajit Aug 7, 2025
fbb75f3
LET-122 | bug: Resume parsing sometimes doesn't return proper start a…
pingSubhajit Aug 7, 2025
5f9e037
LET-125 | comp: Let user upload their existing resume and fill their …
pingSubhajit Aug 13, 2025
79935f3
LET-106 | comp: Display the resume previews with additional details i…
pingSubhajit Aug 14, 2025
6aa4b8e
LET-127 | core: Create Privacy Policy and Terms of Use pages for Letr…
pingSubhajit Aug 16, 2025
ea2cc5e
LET-126 | bug: Validation fails if the AI parses a future date from t…
pingSubhajit Aug 16, 2025
0283791
LET 117 | logic implement search functionality in the client side app…
shadevkumar Aug 18, 2025
ce8290e
LET 116 | core: flow for tailoring a new resume in frontend (#186)
sourabhrathourr Aug 18, 2025
57de462
chore: add contributing & license file
pingSubhajit Aug 19, 2025
742b6bd
feat: inline editing on review viewer for education
sahilverma-dev Jul 17, 2025
1872281
feat: debounce added to inline editing
sahilverma-dev Jul 26, 2025
8e8c355
feat: add SaveIndicator component and implement debounced save functi…
sahilverma-dev Aug 21, 2025
07b88b2
feat: integrate ResumeHighlightContext to conditionally render drag h…
sahilverma-dev Aug 30, 2025
6c82245
refactor: remove console log from AddSectionButton and add TODO for s…
sahilverma-dev Aug 31, 2025
8d8e00e
feat: add current field to EducationData and enhance EducationSection…
sahilverma-dev Aug 31, 2025
c6192bd
feat: enhance EducationSection with escape key handling for editing c…
sahilverma-dev Aug 31, 2025
e98c02c
feat: add optional current field to ExperienceData and define related…
sahilverma-dev Aug 31, 2025
7973034
feat: implement inline editing for ExperienceSection with debounced s…
sahilverma-dev Aug 31, 2025
e440b28
feat: implement inline editing for ProjectsSection and SkillsSection …
sahilverma-dev Aug 31, 2025
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 .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ SENTRY_DSN=

NEXT_PUBLIC_KNOCK_PUBLIC_API_KEY=
NEXT_PUBLIC_KNOCK_FEED_CHANNEL_ID=

NEXT_PUBLIC_RESUME_EDITOR_TABS_NEW_DESIGN_ENABLED=true
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ next-env.d.ts
# IDE
/.idea
/.zed
/.vscode
/.cursor
/.kiro

# Sentry Config File
.env.sentry-build-plugin
31 changes: 31 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Contributing to Letraz

## Current Status

**We are not currently accepting external contributions to this project.**

Letraz is currently being developed by our internal team, and we are not seeking outside contributors at this time.

## Joining Our Core Team

If you're interested in joining our core development team as a contributor, we'd love to hear from you! We're always looking for talented developers who are passionate about building innovative products that reshape how seekers apply for jobs.

**Contact us at:** [hello@letraz.app](mailto:hello@letraz.app)

Please include:
- A brief introduction about yourself
- Your relevant experience and skills
- Why you're interested in joining Letraz
- Any relevant portfolio or GitHub links

## Future Plans

We may open up the project to external contributions in the future. When that happens, we'll update this document with our contribution guidelines and processes.

## Thank You

We appreciate your interest in contributing to Letraz! While we can't accept external contributions right now, we're grateful for the community's enthusiasm and support.

---

*Last updated: August 2025*
4 changes: 2 additions & 2 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2024 Subhajit Kundu
Copyright (c) 2025 Quelac Studios Pvt. Ltd.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand All @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.
286 changes: 286 additions & 0 deletions __tests__/helpers/api-mocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
import {expect, type MockedFunction, vi} from 'vitest'
import {createMockApiError, MockApiError} from './mock-factories'

// API mocking utilities for testing HTTP requests

export interface MockRequestConfig {
method?: string
url?: string | RegExp
status?: number
delay?: number
headers?: Record<string, string>
}

export interface MockResponseConfig<T = any> {
data?: T
status?: number
statusText?: string
headers?: Record<string, string>
delay?: number
}

// Global fetch mock manager
class FetchMockManager {
private mocks: Map<string, any> = new Map()

private defaultMock: any = null

// Set up a mock for a specific URL pattern
mockRequest<T = any>(
pattern: string | RegExp,
response: MockResponseConfig<T> | ((url: string, init?: RequestInit) => MockResponseConfig<T>)
): void {
const key = pattern instanceof RegExp ? pattern.source : pattern
this.mocks.set(key, {pattern, response})
}

// Set up a default mock for all unmatched requests
mockDefault<T = any>(response: MockResponseConfig<T>): void {
this.defaultMock = response
}

// Clear all mocks
clearMocks(): void {
this.mocks.clear()
this.defaultMock = null
}

// Get mock response for a URL
getMockResponse(url: string, init?: RequestInit): MockResponseConfig | null {
// Check specific mocks first
for (const [key, mock] of this.mocks.entries()) {
const {pattern, response} = mock
let matches = false

if (pattern instanceof RegExp) {
matches = pattern.test(url)
} else {
matches = url.includes(pattern)
}

if (matches) {
return typeof response === 'function' ? response(url, init) : response
}
}

// Return default mock if no specific match
return this.defaultMock
}
}

// Global instance
const fetchMockManager = new FetchMockManager()

// Set up the global fetch mock
const setupFetchMock = (): void => {
global.fetch = vi.fn().mockImplementation(async (url: string, init?: RequestInit) => {
const mockConfig = fetchMockManager.getMockResponse(url, init)

if (!mockConfig) {
throw new Error(`No mock configured for URL: ${url}`)
}

// Simulate network delay if specified
if (mockConfig.delay) {
await new Promise(resolve => setTimeout(resolve, mockConfig.delay))
}

const {
data = null,
status = 200,
statusText = 'OK',
headers = {}
} = mockConfig

const response = {
ok: status >= 200 && status < 300,
status,
statusText,
headers: new Headers(headers),
url,
redirected: false,
type: 'basic' as ResponseType,
body: null,
bodyUsed: false,
clone: vi.fn(),
json: vi.fn().mockResolvedValue(data),
text: vi.fn().mockResolvedValue(JSON.stringify(data)),
arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(0)),
blob: vi.fn().mockResolvedValue(new Blob()),
formData: vi.fn().mockResolvedValue(new FormData()),
bytes: vi.fn().mockResolvedValue(new Uint8Array())
}

return response as Response
})
}

// API mock utilities
export const apiMocks = {
// Set up fetch mock
setup: setupFetchMock,

// Mock a successful API response
mockSuccess: <T = any>(
url: string | RegExp,
data: T,
options: Omit<MockResponseConfig<T>, 'data'> = {}
): void => {
fetchMockManager.mockRequest(url, {
data,
status: 200,
statusText: 'OK',
...options
})
},

// Mock an API error response
mockError: (
url: string | RegExp,
error: Partial<MockApiError> = {},
options: Omit<MockResponseConfig, 'data'> = {}
): void => {
const errorResponse = createMockApiError(error)
fetchMockManager.mockRequest(url, {
data: errorResponse,
status: error.status || 500,
statusText: error.statusText || 'Internal Server Error',
...options
})
},

// Mock network failure
mockNetworkError: (url: string | RegExp, message = 'Network Error'): void => {
fetchMockManager.mockRequest(url, () => {
throw new Error(message)
})
},

// Mock timeout
mockTimeout: (url: string | RegExp, delay = 5000): void => {
fetchMockManager.mockRequest(url, {
data: null,
delay,
status: 408,
statusText: 'Request Timeout'
})
},

// Mock different HTTP methods
mockGet: <T = any>(url: string | RegExp, data: T, options?: MockResponseConfig<T>): void => {
apiMocks.mockSuccess(url, data, options)
},

mockPost: <T = any>(url: string | RegExp, data: T, options?: MockResponseConfig<T>): void => {
apiMocks.mockSuccess(url, data, {status: 201, statusText: 'Created', ...options})
},

mockPut: <T = any>(url: string | RegExp, data: T, options?: MockResponseConfig<T>): void => {
apiMocks.mockSuccess(url, data, options)
},

mockDelete: (url: string | RegExp, options?: MockResponseConfig): void => {
apiMocks.mockSuccess(url, null, {status: 204, statusText: 'No Content', ...options})
},

// Mock paginated responses
mockPaginated: <T = any>(
url: string | RegExp,
items: T[],
page = 1,
limit = 10,
total?: number
): void => {
const totalItems = total || items.length
const totalPages = Math.ceil(totalItems / limit)
const startIndex = (page - 1) * limit
const endIndex = startIndex + limit
const paginatedItems = items.slice(startIndex, endIndex)

const paginatedResponse = {
data: paginatedItems,
pagination: {
page,
limit,
total: totalItems,
totalPages,
hasNext: page < totalPages,
hasPrev: page > 1
}
}

apiMocks.mockSuccess(url, paginatedResponse)
},

// Clear all mocks
clearAll: (): void => {
fetchMockManager.clearMocks()
vi.clearAllMocks()
},

// Reset fetch mock
reset: (): void => {
apiMocks.clearAll()
setupFetchMock()
},

// Verify fetch was called
verifyFetchCalled: (url?: string | RegExp, times?: number): void => {
const fetchMock = global.fetch as MockedFunction<typeof fetch>

if (url) {
const calls = fetchMock.mock.calls.filter(call => {
const callUrl = call[0] as string
if (url instanceof RegExp) {
return url.test(callUrl)
}
return callUrl.includes(url)
})

if (times !== undefined) {
expect(calls).toHaveLength(times)
} else {
expect(calls.length).toBeGreaterThan(0)
}
} else {
if (times !== undefined) {
expect(fetchMock).toHaveBeenCalledTimes(times)
} else {
expect(fetchMock).toHaveBeenCalled()
}
}
},

// Get fetch call arguments
getFetchCalls: (): Array<[string, RequestInit?]> => {
const fetchMock = global.fetch as MockedFunction<typeof fetch>
return fetchMock.mock.calls as Array<[string, RequestInit?]>
},

// Mock specific endpoints commonly used in the app
mockAuth: {
login: (user: any) => apiMocks.mockPost('/api/auth/login', {user, token: 'mock-token'}),
logout: () => apiMocks.mockPost('/api/auth/logout', {success: true}),
refresh: (token: string) => apiMocks.mockPost('/api/auth/refresh', {token}),
me: (user: any) => apiMocks.mockGet('/api/auth/me', {user})
},

mockResume: {
list: (resumes: any[]) => apiMocks.mockGet('/api/resumes', resumes),
get: (resume: any) => apiMocks.mockGet(/\/api\/resumes\/\w+/, resume),
create: (resume: any) => apiMocks.mockPost('/api/resumes', resume),
update: (resume: any) => apiMocks.mockPut(/\/api\/resumes\/\w+/, resume),
delete: () => apiMocks.mockDelete(/\/api\/resumes\/\w+/)
},

mockJob: {
list: (jobs: any[]) => apiMocks.mockGet('/api/jobs', jobs),
get: (job: any) => apiMocks.mockGet(/\/api\/jobs\/\w+/, job),
search: (jobs: any[]) => apiMocks.mockGet('/api/jobs/search', jobs)
}
}

// Initialize fetch mock
apiMocks.setup()

export {fetchMockManager}
Loading