Skip to content

Commit 38526ea

Browse files
authored
style: improve create/edit listing page styling (#547)
Made the IOM link less intrusive and made form components more consistent with one another.
2 parents 66ba8fa + 4d6cff1 commit 38526ea

5 files changed

Lines changed: 198 additions & 187 deletions

File tree

.gitignore

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,10 @@
2121
# OS files
2222
**/.DS_Store
2323
*.log
24-
package-lock.json
24+
package-lock.json
25+
26+
# Agents files
27+
CLAUDE.md
28+
AGENTS.md
29+
.claude/
30+
.claudeignore

components/EditForm.tsx

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
Popover,
1414
PopoverContent,
1515
PopoverTrigger,
16+
AnimatedCount,
1617
} from "@betterinternship/components";
1718
import { Calendar } from "@/components/ui/calendar";
1819
import {
@@ -25,6 +26,7 @@ import * as RadioGroup from "@radix-ui/react-radio-group";
2526
import {
2627
CalendarDays,
2728
Check,
29+
CheckCheck,
2830
ChevronDown,
2931
ChevronLeft,
3032
ChevronRight,
@@ -33,14 +35,14 @@ import {
3335
Minus,
3436
} from "lucide-react";
3537
import * as React from "react";
36-
import { createContext, useContext, useRef } from "react";
38+
import { createContext, useContext, useEffect, useRef } from "react";
3739
import "react-datepicker/dist/react-datepicker.css";
3840
import { GroupableRadioDropdown } from "./ui/dropdown";
3941
import { Input } from "@betterinternship/components";
4042
import { Tooltip } from "react-tooltip";
4143
import { Textarea } from "./ui/textarea";
4244
import { Matcher } from "react-day-picker";
43-
import { AnimatePresence, motion } from "framer-motion";
45+
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
4446
import { useBlurTransition } from "./animata/blur";
4547

4648
interface EditFormContext<T extends IFormData> {
@@ -439,14 +441,26 @@ export const FormCheckbox = ({
439441
interface FormCheckBoxGroupProps extends React.InputHTMLAttributes<HTMLInputElement> {
440442
options: { value: string | number; label: string; description?: string }[];
441443
values: (string | number)[];
442-
setter: (value: any) => void;
444+
setter: (values: (string | number)[]) => void;
443445
label?: string;
444446
required?: boolean;
445447
className?: string;
446448
tooltip?: string;
447449
tooltipId?: string;
450+
/** Multi-select hint shown above the options. Pass `null` to hide it. */
451+
hint?: React.ReactNode;
452+
/** Columns on md+ screens. Defaults to 3. */
453+
columns?: 1 | 2 | 3;
454+
/** Fired when a single option is toggled, before the setter runs. */
455+
onToggle?: (value: string | number, checked: boolean) => void;
448456
}
449457

458+
const GRID_COLS: Record<1 | 2 | 3, string> = {
459+
1: "",
460+
2: "md:grid-cols-2",
461+
3: "md:grid-cols-3",
462+
};
463+
450464
export const FormCheckBoxGroup = ({
451465
options,
452466
values,
@@ -456,16 +470,23 @@ export const FormCheckBoxGroup = ({
456470
className,
457471
tooltip,
458472
tooltipId,
473+
hint = "Select all that apply",
474+
columns = 3,
475+
onToggle,
459476
...props
460477
}: FormCheckBoxGroupProps) => {
461478
const handleValueChange = (optionValue: string | number) => {
462-
if (values.includes(optionValue)) {
463-
setter(values.filter((v) => v !== optionValue));
464-
} else {
465-
setter([...values, optionValue]);
466-
}
479+
const willBeChecked = !values.includes(optionValue);
480+
onToggle?.(optionValue, willBeChecked);
481+
setter(
482+
willBeChecked
483+
? [...values, optionValue]
484+
: values.filter((v) => v !== optionValue),
485+
);
467486
};
468487

488+
const blurTransition = useBlurTransition();
489+
469490
return (
470491
<div className={cn("space-y-3", className)}>
471492
{label && (
@@ -477,16 +498,43 @@ export const FormCheckBoxGroup = ({
477498
/>
478499
)}
479500

480-
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
501+
{(hint || values.length > 0) && (
502+
<div className="flex items-center gap-2 text-xs">
503+
{hint && (
504+
<span className="inline-flex items-center gap-1 rounded-full bg-muted px-2 py-0.5 font-medium text-muted-foreground">
505+
<CheckCheck className="h-3 w-3" />
506+
{hint}
507+
</span>
508+
)}
509+
<AnimatePresence>
510+
{values.length > 0 && (
511+
<motion.span
512+
className="font-medium text-primary"
513+
{...blurTransition}
514+
>
515+
<AnimatedCount value={values.length} /> selected
516+
</motion.span>
517+
)}
518+
</AnimatePresence>
519+
</div>
520+
)}
521+
522+
<div className={cn("grid grid-cols-1 gap-4", GRID_COLS[columns])}>
481523
{options.map((option) => {
482524
const isChecked = values.includes(option.value);
483525

484526
return (
485527
<div
486528
key={option.value}
529+
role="checkbox"
530+
aria-checked={isChecked}
487531
onClick={() => handleValueChange(option.value)}
488-
className={`flex items-start gap-4 p-3 border rounded-[0.33em] transition-colors cursor-pointer h-fit
489-
${isChecked ? "border-primary border-opacity-85" : "border-gray-200 hover:border-gray-300"}`}
532+
className={cn(
533+
"flex items-start gap-3 p-3 border rounded-[0.33em] transition-colors cursor-pointer h-fit",
534+
isChecked
535+
? "border-primary border-opacity-85 bg-primary/5"
536+
: "border-gray-200 hover:border-gray-300 hover:bg-gray-50",
537+
)}
490538
>
491539
<FormCheckbox checked={isChecked ?? false} />
492540
<div className="grid grid-rows-1 md:grid-rows-2">

components/features/hire/listings/create-job-steps/BasicStep.tsx

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { FormInput } from "@/components/EditForm";
44
import { GroupableRadioDropdown } from "@/components/ui/dropdown";
55
import { Job } from "@/lib/db/db.types";
6-
import { Card, PageHeader } from "@betterinternship/components";
6+
import { AnimatedCount, Card, PageHeader } from "@betterinternship/components";
77
import { Input } from "@betterinternship/components";
88
import { Textarea } from "@/components/ui/textarea";
99
import { BasicStepIllustration } from "./illustrations/BasicStepIllustration";
@@ -99,15 +99,10 @@ const BasicStep = ({
9999
maxLength={100}
100100
className="h-10"
101101
/>
102-
<div className="flex justify-between gap-4">
103-
<p className="text-xs text-muted-foreground">
104-
Keep it clear and specific. “Frontend Intern” works better than
105-
“Intern”.
106-
</p>
107-
<p className="text-xs text-muted-foreground tabular-nums shrink-0">
108-
{titleLength}/100
109-
</p>
110-
</div>
102+
<p className="text-xs text-muted-foreground tabular-nums shrink-0 text-right">
103+
<AnimatedCount value={titleLength} />
104+
/100
105+
</p>
111106
</div>
112107

113108
{/* Location */}
@@ -121,9 +116,6 @@ const BasicStep = ({
121116
maxLength={100}
122117
className="h-10"
123118
/>
124-
<p className="text-xs text-muted-foreground">
125-
Where will the intern work? You can change this later.
126-
</p>
127119
</div>
128120

129121
{/* Category */}
@@ -134,10 +126,6 @@ const BasicStep = ({
134126
</span>
135127
<span className="text-destructive text-xs">*</span>
136128
</div>
137-
<p className="text-xs text-muted-foreground -mt-1">
138-
Choose a category that describes the job (like Cybersecurity, Legal,
139-
Design, etc.).
140-
</p>
141129
<GroupableRadioDropdown
142130
name="category"
143131
defaultValue={categoryValue}

0 commit comments

Comments
 (0)