Skip to content
Open
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
5 changes: 4 additions & 1 deletion apps/web/src/app/_components/onboarding/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { useState } from "react";

import type { Session } from "@cooper/auth";
import { Dialog, DialogContent } from "@cooper/ui/dialog";
import { Dialog, DialogContent, DialogTitle } from "@cooper/ui/dialog";

import { OnboardingForm } from "~/app/_components/onboarding/onboarding-form";
import { api } from "~/trpc/react";
Expand Down Expand Up @@ -70,6 +70,9 @@ export function OnboardingDialog({
onInteractOutside={(e) => e.preventDefault()}
onEscapeKeyDown={(e) => e.preventDefault()}
>
<DialogTitle className="sr-only">
Create your Cooper profile
</DialogTitle>
<OnboardingForm
userId={session.user.id}
closeDialog={closeDialog}
Expand Down
27 changes: 24 additions & 3 deletions apps/web/src/app/_components/reviews/existing-company-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Fuse from "fuse.js";
import { useForm, useFormContext } from "react-hook-form";
import { z } from "zod";

import { cn } from "@cooper/ui";
import { Button } from "@cooper/ui/button";
import { Checkbox } from "@cooper/ui/checkbox";
import { FormControl, FormField, FormItem, FormMessage } from "@cooper/ui/form";
Expand All @@ -18,8 +19,10 @@ import { CompanyCardPreview } from "../companies/company-card-preview";
import { FormSection } from "../form/form-section";
import LocationBox from "../location";
import { industryOptions } from "../onboarding/constants";

import { FormLabel } from "../themed/onboarding/form";
import FilterBody from "../filters/filter-body";
import { findRestrictedRoleWord } from "~/utils/stringHelpers";

const filter = new Filter();
const roleSchema = z.object({
Expand All @@ -30,7 +33,13 @@ const roleSchema = z.object({
})
.refine((val) => !filter.isProfane(val), {
message: "The title cannot contain profane words.",
}),
})
.refine(
(val) => findRestrictedRoleWord(val) === null,
(val) => ({
message: `The title cannot contain the word "${findRestrictedRoleWord(val)}".`,
}),
),
// description: z
// .string()
// .min(10, {
Expand Down Expand Up @@ -242,7 +251,7 @@ export default function ExistingCompanyContent({
// Only validate the title field since companyId and createdBy are set programmatically
const isTitleValid = await newRoleForm.trigger("title");
if (!isTitleValid) {
const errorMessage = newRoleForm.formState.errors.title?.message;
const errorMessage = newRoleForm.getFieldState("title").error?.message;
toast.error(errorMessage ?? "Please enter a valid role title.");
return;
}
Expand Down Expand Up @@ -310,6 +319,10 @@ export default function ExistingCompanyContent({
} else {
field.onChange(undefined);
setSelectedCompanyId(undefined);
// Clear any role selection tied to the now-unselected company
form.setValue("roleName", "");
setCreatingNewRole(false);
newRoleForm.setValue("title", "");
}
setCompanySearchTerm("");
}}
Expand Down Expand Up @@ -558,6 +571,7 @@ export default function ExistingCompanyContent({

<div className="flex flex-1 items-center gap-2 pt-2">
<Checkbox
disabled={!selectedCompanyId}
checked={creatingNewRole}
onCheckedChange={(checked) => {
setCreatingNewRole(checked === true);
Expand All @@ -567,7 +581,14 @@ export default function ExistingCompanyContent({
}
}}
/>
<Label className="text-cooper-gray-550 cursor-pointer text-sm font-bold">
<Label
className={cn(
"text-cooper-gray-550 text-sm font-bold",
selectedCompanyId
? "cursor-pointer"
: "cursor-not-allowed opacity-50",
)}
>
I don't see my role
</Label>
</div>
Expand Down
10 changes: 9 additions & 1 deletion apps/web/src/app/_components/reviews/review-view-edit-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ import {
ZodInterviewTypeSchema,
} from "@cooper/db/schema";
import { useCustomToast } from "@cooper/ui";
import { Dialog, DialogClose, DialogContent } from "@cooper/ui/dialog";
import {
Dialog,
DialogClose,
DialogContent,
DialogTitle,
} from "@cooper/ui/dialog";
import { Form } from "@cooper/ui/form";

import {
Expand Down Expand Up @@ -539,6 +544,9 @@ export function ReviewViewEditModal({
}
}}
>
<DialogTitle className="sr-only">
{mode === "view" ? (role?.title ?? "Review") : "Edit Review"}
</DialogTitle>
{/* Header */}
<div className="flex md:flex-row flex-col shrink-0 items-center justify-between bg-cooper-gray-700 pb-5 pl-6 pr-6 pt-8">
<div className="hidden md:block">
Expand Down
6 changes: 6 additions & 0 deletions apps/web/src/utils/stringHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import type { WorkEnvironmentType } from "@cooper/db/schema";
import { cn } from "@cooper/ui";

const RESTRICTED_ROLE_WORD_RE = /\b(internship|intern|co-?op)\b/i;

export function findRestrictedRoleWord(title: string): string | null {
return RESTRICTED_ROLE_WORD_RE.exec(title)?.[0] ?? null;
}

export function truncateText(text: string, length: number): string {
return text && text.length >= length
? cn(text.slice(0, length), "...")
Expand Down
1 change: 1 addition & 0 deletions packages/db/package.json

@songmichael11 songmichael11 Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so like should devs just run pnpm db:strip-role-keywords then? does this apply the changes cause wouldn't you need the --apply instead of just pnpm with-env tsx src/scripts/strip-role-keywords.ts

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pnpm strip-role-keywords --apply

Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"migrate": "pnpm with-env drizzle-kit migrate",
"push": "pnpm with-env drizzle-kit push",
"studio": "pnpm with-env drizzle-kit studio",
"strip-role-keywords": "pnpm with-env tsx src/scripts/strip-role-keywords.ts",
"typecheck": "tsc --noEmit --emitDeclarationOnly false",
"with-env": "dotenv -e ../../.env --"
},
Expand Down
122 changes: 122 additions & 0 deletions packages/db/src/scripts/strip-role-keywords.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/**
* Data migration: strip the keywords "coop", "co-op", "intern", and
* "internship" (case-insensitive) out of every role title, then regenerate
* the role's slug from the cleaned title.
*
* Usage (run from packages/db):
* pnpm strip-role-keywords # dry run – prints what WOULD change
* pnpm strip-role-keywords --apply # actually writes the changes
*/
import { eq } from "drizzle-orm";

import { db } from "../client";
import { Role } from "../schema";

const APPLY = process.argv.includes("--apply");

const KEYWORD_RE = /\b(?:internship|intern|co-?op)\b/gi;

const EDGE_JUNK_RE = /^[\s\-/,&|·•:]+|[\s\-/,&|·•:]+$/g;

function stripKeywords(title: string): string {
return title
.replace(KEYWORD_RE, " ")
.replace(/\s+/g, " ")
.trim()
.replace(EDGE_JUNK_RE, "")
.trim();
}

function createSlug(text: string): string {
return text
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, "")
.replace(/\s+/g, "-")
.replace(/-+/g, "-")
.trim();
}

function generateUniqueSlug(
baseSlug: string,
existingSlugs: Set<string>,
): string {
let slug = baseSlug;
let counter = 2;
while (existingSlugs.has(slug)) {
slug = `${baseSlug}-${counter}`;
counter++;
}
return slug;
}

async function main() {
console.log(
`\nStripping role keywords — ${APPLY ? "APPLY mode (writing changes)" : "DRY RUN (no changes written)"}\n`,
);

const roles = await db.query.Role.findMany({
columns: { id: true, title: true, slug: true, companyId: true },
});

// Track slugs already in use per company so regenerated slugs stay unique
// within the company, accounting for both untouched and newly-written rows.
const slugsByCompany = new Map<string, Set<string>>();
for (const r of roles) {
if (!slugsByCompany.has(r.companyId))
slugsByCompany.set(r.companyId, new Set());
slugsByCompany.get(r.companyId)?.add(r.slug);
}

let changed = 0;
let skippedEmpty = 0;

for (const role of roles) {
const newTitle = stripKeywords(role.title);

if (newTitle === role.title) continue;

if (newTitle.length === 0) {
skippedEmpty++;
console.warn(
` ⚠️ SKIPPED (would be empty): "${role.title}" [role ${role.id}]`,
);
continue;
}

const companySlugs = slugsByCompany.get(role.companyId);
if (!companySlugs) {
throw new Error(`Missing slug set for company ${role.companyId}`);
}
companySlugs.delete(role.slug);
const newSlug = generateUniqueSlug(createSlug(newTitle), companySlugs);
companySlugs.add(newSlug);

changed++;
console.log(
` "${role.title}" -> "${newTitle}" (slug: ${role.slug} -> ${newSlug})`,
);

if (APPLY) {
await db
.update(Role)
.set({ title: newTitle, slug: newSlug })
.where(eq(Role.id, role.id));
}
}

console.log(
`\nScanned ${roles.length} roles — ${changed} ${APPLY ? "updated" : "to update"}${
skippedEmpty ? `, ${skippedEmpty} skipped (would be empty)` : ""
}.`,
);
if (!APPLY && changed > 0) {
console.log(`Re-run with --apply to write these changes.\n`);
}
}

main()
.then(() => process.exit(0))
.catch((err) => {
console.error("strip-role-keywords failed:", err);
process.exit(1);
});
Loading