Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
145 changes: 123 additions & 22 deletions apps/backend/scripts/seed-mock-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,31 @@ const REVIEW_STATUS_VALUES = Object.values(ReviewStatus);
const POSITIONS = Object.values(Position);
const SEMESTER_DEFAULT = Semester.SPRING;
const YEAR_DEFAULT = 2026;
const REVIEW_COMMENTS = [
'Strong analytical breakdown with measurable success metrics.',
'Creative approach to ambiguous prompts and thoughtful user empathy.',
'Great initiative but would like deeper dives into tradeoffs.',
'Excellent presentation polish and storytelling under time pressure.',
'Solid collaboration signals; digs into blockers proactively.',
'Needs more rigor around experimentation but strategy instincts are sound.',
'Powerful technical depth with clean architecture diagrams.',
'Great culture add with emphasis on mentoring and peer support.',
];
const REVIEW_FOCUS_AREAS = [
'product sense',
'execution',
'communication',
'technical depth',
'user research',
'data fluency',
'leadership',
];
const DAY_IN_MS = 24 * 60 * 60 * 1000;
const ROLE_LABELS: Record<Position, string> = {
[Position.DEVELOPER]: 'Developer',
[Position.PM]: 'Product Manager',
[Position.DESIGNER]: 'Designer',
};

async function main() {
const envLoaded = loadEnvIfPresent('../.env');
Expand Down Expand Up @@ -120,9 +145,10 @@ async function main() {
users,
config.personalUser,
);
const reviews = await seedReviews(dataSource, applications, users);

console.log(
`Seed complete. Inserted ${users.length} users and ${applications.length} applications.`,
`Seed complete. Inserted ${users.length} users, ${applications.length} applications, and ${reviews.length} reviews.`,
);
} catch (error) {
console.error('Mock data seed failed:', error);
Expand Down Expand Up @@ -231,16 +257,7 @@ async function seedApplications(
const createdAt = new Date(
Date.UTC(YEAR_DEFAULT, (index * 3) % 12, (index % 27) + 1, 12, 0, 0),
);
const response: Response[] = [
{
question: 'Why do you want to work on this role?',
answer: `I am excited to contribute as a ${position} during the ${semester} semester.`,
},
{
question: 'What stage best suits you right now?',
answer: `Currently focused on ${stage} with ${stageProgress} progress.`,
},
];
const response = buildApplicationResponses(owner, position);

return appRepo.create({
user: owner,
Expand Down Expand Up @@ -268,17 +285,7 @@ async function seedApplications(
stage: ApplicationStage.PM_CHALLENGE,
stageProgress: StageProgress.PENDING,
reviewStatus: ReviewStatus.UNASSIGNED,
response: [
{
question: 'Who owns this application?',
answer: `${personalUser.firstName} ${personalUser.lastName} (${personalUser.email}).`,
},
{
question: 'Why is this record seeded?',
answer:
'This hardcoded row is appended for the personal user defined in seed.config.json.',
},
],
response: buildApplicationResponses(personalUser, Position.PM),
assignedRecruiterIds:
recruiterIds.length === 0
? []
Expand All @@ -292,6 +299,100 @@ async function seedApplications(
return appRepo.save(applications);
}

async function seedReviews(
dataSource: DataSource,
applications: Application[],
users: User[],
): Promise<Review[]> {
const reviewRepo = dataSource.getRepository(Review);
const recruiterMap = new Map<number, User>(
users
.filter((user) => user.status === UserStatus.RECRUITER)
.map((recruiter) => [recruiter.id, recruiter]),
);

const reviews: Review[] = [];

applications.forEach((application, index) => {
const assigned = application.assignedRecruiterIds ?? [];
if (!assigned.length) {
return;
}

const reviewCount = index % 3; // cycle through 0, 1, 2 reviews per applicant

for (let offset = 0; offset < reviewCount; offset++) {
const reviewerId = assigned[(index + offset) % assigned.length];
const reviewer = recruiterMap.get(reviewerId);
if (!reviewer) {
continue;
}

const stage =
APPLICATION_STAGES[
(index + reviewerId + offset) % APPLICATION_STAGES.length
];
const ratingBase =
1 + ((index * 17 + reviewerId * 13 + offset * 7) % 40) / 10;
const rating = Number(Math.min(5, ratingBase).toFixed(1));
const commentSeed =
(index + reviewerId + offset) % REVIEW_COMMENTS.length;
const focus =
REVIEW_FOCUS_AREAS[(index + offset) % REVIEW_FOCUS_AREAS.length];
const content = `${REVIEW_COMMENTS[commentSeed]} Focused on ${focus} for the ${application.position} track.`;
const baseDate = application.createdAt
? new Date(application.createdAt)
: new Date(Date.UTC(YEAR_DEFAULT, 0, 1));
const createdAt = new Date(baseDate.getTime() + (offset + 1) * DAY_IN_MS);

reviews.push(
reviewRepo.create({
application,
reviewer,
reviewerId,
rating,
stage,
content,
createdAt,
updatedAt: createdAt,
}),
);
}
});

console.log(`Generating ${reviews.length} review rows...`);
if (!reviews.length) {
return [];
}

return reviewRepo.save(reviews);
}

function buildApplicationResponses(
owner: User,
position: Position,
): Response[] {
return [
{
question: 'Full Name',
answer: buildFullName(owner),
},
{
question: 'Email',
answer: owner.email,
},
{
question: 'Role',
answer: ROLE_LABELS[position],
},
];
}

function buildFullName(user: User): string {
const name = [user.firstName, user.lastName].filter(Boolean).join(' ').trim();
return name || 'Unknown Applicant';
}

function buildNamePairs(): Array<{ firstName: string; lastName: string }> {
const pairs: Array<{ firstName: string; lastName: string }> = [];
FIRST_NAMES.forEach((firstName) => {
Expand Down
4 changes: 3 additions & 1 deletion apps/backend/src/applications/applications.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@
Position,
ApplicationStage,
StageProgress,
Semester,

Check warning on line 24 in apps/backend/src/applications/applications.service.ts

View workflow job for this annotation

GitHub Actions / pre-deploy

'Semester' is defined but never used
} from '@shared/types/application.types';
import * as crypto from 'crypto';
import { User } from '../users/user.entity';
import { forEach } from 'lodash';

Check warning on line 28 in apps/backend/src/applications/applications.service.ts

View workflow job for this annotation

GitHub Actions / pre-deploy

'forEach' is defined but never used
import { Review } from '../reviews/review.entity';
import { UserStatus } from '@shared/types/user.types';
import { stagesMap } from './applications.constants';
Expand Down Expand Up @@ -73,7 +73,9 @@

// Determine position from provided role (if any). Default to DEVELOPER.
let positionEnum = Position.DEVELOPER;
this.logger.debug(`submitApp called with role='${role}' for user ${user.email}`);
this.logger.debug(
`submitApp called with role='${role}' for user ${user.email}`,
);
if (role) {
const r = (role || '').toString().toUpperCase();
if (r === 'PM' || r === 'PRODUCT_MANAGER' || r === 'PRODUCT MANAGER') {
Expand Down Expand Up @@ -521,7 +523,7 @@
*/
private determineStageProgress(
app: Application,
reviews: any[],

Check warning on line 526 in apps/backend/src/applications/applications.service.ts

View workflow job for this annotation

GitHub Actions / pre-deploy

Unexpected any. Specify a different type
): StageProgress {
const stage = app.stage;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export const ApplicantView = ({ user }: ApplicantViewProps) => {
borderRadius: 2,
boxShadow: 2,
width: { xs: '95%', md: '70%' },
maxWidth: 900,
maxWidth: 700,
position: 'relative',
zIndex: 1,
boxSizing: 'border-box',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
getRecruitmentYear as getCurrentYear,
} from '@sharedTypes/utils/cycle';
import {
dataGridStyles,
defaultPaginationModel,
defaultPageSizeOptions,
} from '@styles/dataGridTheme';
Expand All @@ -32,49 +33,84 @@ export function ApplicationTable() {
const showEmpty = !isLoading && !error && data.length === 0;

return (
<Container maxWidth="xl">
<Stack direction="row" alignItems="center" spacing={2} mt={4} mb={8}>
<img
src={LOGO_PATHS.SQUARE}
alt="C4C Logo"
style={{ width: 50, height: 40 }}
/>
<Typography variant="h4" sx={{ fontWeight: 'bold', color: 'white' }}>
Database | {getCurrentSemester()} {getCurrentYear()} Recruitment Cycle
</Typography>
</Stack>
{showEmpty ? (
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: 300,
border: '1px dashed rgba(255,255,255,0.2)',
borderRadius: 2,
color: 'rgba(255,255,255,0.85)',
background: 'rgba(255,255,255,0.02)',
}}
<Box
sx={{
minHeight: '100vh',
backgroundColor: '#181818',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
}}
>
<Container
maxWidth="xl"
sx={{
flex: 1,
display: 'flex',
flexDirection: 'column',
gap: 4,
py: 4,
minHeight: 0,
}}
>
<Stack
direction="row"
alignItems="center"
spacing={2}
sx={{ flexShrink: 0 }}
>
<Typography variant="h6" sx={{ fontWeight: 500 }}>
There are no applications at this time
<img
src={LOGO_PATHS.SQUARE}
alt="C4C Logo"
style={{ width: 50, height: 40 }}
/>
<Typography variant="h4" sx={{ fontWeight: 'bold', color: 'white' }}>
Database | {getCurrentSemester()} {getCurrentYear()} Recruitment
Cycle
</Typography>
</Box>
) : (
<DataGrid
rows={data}
columns={applicationColumns(allRecruiters)}
initialState={{
pagination: {
paginationModel: defaultPaginationModel,
},
}}
pageSizeOptions={defaultPageSizeOptions}
onRowClick={handleRowClick}
disableRowSelectionOnClick
sx={{ cursor: 'pointer' }}
/>
)}
</Container>
</Stack>

{showEmpty ? (
<Box
sx={{
flex: 1,
minHeight: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: '1px dashed rgba(255,255,255,0.2)',
borderRadius: 2,
color: 'rgba(255,255,255,0.85)',
background: 'rgba(255,255,255,0.02)',
}}
>
<Typography variant="h6" sx={{ fontWeight: 500 }}>
There are no applications at this time
</Typography>
</Box>
) : (
<Box sx={{ flex: 1, minHeight: 0 }}>
<DataGrid
rows={data}
columns={applicationColumns(allRecruiters)}
initialState={{
pagination: {
paginationModel: defaultPaginationModel,
},
}}
pageSizeOptions={defaultPageSizeOptions}
onRowClick={handleRowClick}
disableRowSelectionOnClick
sx={{
...dataGridStyles,
cursor: 'pointer',
height: '100%',
width: '100%',
}}
/>
</Box>
)}
</Container>
</Box>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,7 @@ const IndividualApplicationDetails = ({
)}
</Grid>

<Grid container spacing={1.5}>
<Grid container spacing={1.5} sx={{ mt: 2 }}>
<Grid item xs={12} md={8}>
<Stack
direction="column"
Expand All @@ -413,7 +413,6 @@ const IndividualApplicationDetails = ({
p: { xs: 2, md: 2.5 },
backgroundColor: 'transparent',
gap: 1,
mt: 2,
}}
>
<Typography
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/features/homepage/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const Header = ({ isAuthenticated, onSignOut }: HeaderProps) => {
style={{ height: '40px', width: 'auto' }}
/>
<Typography variant="h5" sx={{ fontWeight: 500 }}>
2025 Application
2026 Application
</Typography>
</Box>

Expand Down
7 changes: 3 additions & 4 deletions apps/frontend/src/shared/pages/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const HomePage = () => {
const { isAuthenticated, signOut } = useAuth();

// Application deadline
const deadline = new Date('2025-10-31T23:59:59');
const deadline = new Date('2026-01-01T23:59:59');

return (
<Box
Expand All @@ -36,8 +36,8 @@ const HomePage = () => {
>
<Header isAuthenticated={isAuthenticated} onSignOut={signOut} />

<Container maxWidth="md" sx={{ flex: 1, py: 8 }}>
<Stack spacing={4}>
<Container maxWidth="md" sx={{ py: 3 }}>
<Stack spacing={2}>
<WelcomeBanner />
<RoleSelector />
<DeadlineCountdown deadline={deadline} />
Expand All @@ -46,5 +46,4 @@ const HomePage = () => {
</Box>
);
};

export default HomePage;
3 changes: 3 additions & 0 deletions apps/frontend/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ html, body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
margin: 0;
padding: 0;
overflow-x: hidden;
}

#root {
Expand Down
Loading
Loading