Skip to content

Commit

Permalink
Replace tree-style skills with tags (#1083)
Browse files Browse the repository at this point in the history
* Add first draft of skills-to-tags conversion routine

* Add support for “tags” field in user profile

* Add first version of hashtag select component, use in registration form

* Refactor types

* Reshuffle registration form fields

* Fix error in user creation endpoint

* Add standalone user profile field for max seniority

* Improve copy

* Replace old skill selection with tag select in user profile

* Trivial refactoring

* Share occupation select between user profile and registration page

* Add first batch of new skill tags

* Improve copy & layout consistency between registration form and profile page

* Fix trivial UI bugs in skill select

* Make “background” registration field optional

* Display user tags from new field

* Update user creation endpoint to use new fields

* Improve skill select UI

* Add non-immediate mode to skill select

* Add helper script to sort skill tags

* Update skill tags

* Try a different skill presentation option

* Update tag list

* Limit tag list on /people to 6 lines

* Fix typo

* Add temporary skill migration script

* Remove old tree-style skill picker

* Improve skill layout on user profile page

* Trivial refactoring
  • Loading branch information
zoul authored Sep 30, 2024
1 parent 4dfcbd5 commit 7909ffa
Show file tree
Hide file tree
Showing 25 changed files with 897 additions and 622 deletions.
1 change: 0 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
*.md
*.tf
*.json
193 changes: 61 additions & 132 deletions app/account/UserProfileTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,12 @@ import { CopyToClipboardButton } from "~/components/CopyToClipboardButton";
import { DistrictSelect } from "~/components/districts/DistrictSelect";
import { FormError } from "~/components/form/FormError";
import { usePatchedJSONResource } from "~/components/hooks/resource";
import { SkillPicker } from "~/components/SkillPicker";
import { OccupationSelect } from "~/components/profile/OccupationSelect";
import { SenioritySelect } from "~/components/profile/SenioritySelect";
import { SkillSelect } from "~/components/profile/SkillSelect";
import { type UserProfile } from "~/src/data/user-profile";
import { setFlag } from "~/src/flags";
import { absolute, Route } from "~/src/routing";
import {
decodeSkillSelection,
encodeSkillSelection,
type SkillSelection,
} from "~/src/skills/skills";
import skills from "~/src/skills/skills.json";
import { looksLikeEmailAdress } from "~/src/utils";

type SectionProps = {
Expand All @@ -42,16 +38,18 @@ export const UserProfileTab = () => {

return (
<div className="flex flex-col gap-10">
<BioSection model={model} updating={updating} onChange={setModel} />
<BasicInfoSection model={model} updating={updating} onChange={setModel} />
<PrivacySection model={model} updating={updating} onChange={setModel} />
<WorkSection model={model} updating={updating} onChange={setModel} />
<SkillSection model={model} updating={updating} onChange={setModel} />
<MapSection model={model} onChange={setModel} />
<DetailedInfoSection
model={model}
updating={updating}
onChange={setModel}
/>
</div>
);
};

const BioSection = ({ model, updating, onChange }: SectionProps) => {
const BasicInfoSection = ({ model, updating, onChange }: SectionProps) => {
return (
<section className="flex max-w-prose flex-col gap-7">
<h2 className="typo-title2">Základní informace</h2>
Expand Down Expand Up @@ -125,6 +123,57 @@ const BioSection = ({ model, updating, onChange }: SectionProps) => {
);
};

const DetailedInfoSection = ({ model, updating, onChange }: SectionProps) => (
<section className="flex max-w-prose flex-col gap-7">
<h2 className="typo-title2">Řekni nám o sobě víc</h2>

<SkillSelect
onChange={(tags) => onChange({ ...model!, tags })}
value={model?.tags ?? ""}
disabled={updating}
/>

<SenioritySelect
onChange={(maxSeniority) => onChange({ ...model!, maxSeniority })}
value={model?.maxSeniority}
disabled={updating}
/>

<OccupationSelect
onChange={(occupation) => onChange({ ...model!, occupation })}
occupation={model?.occupation}
disabled={updating}
/>

<InputWithSaveButton
onSave={(organizationName) => onChange({ ...model!, organizationName })}
id="organizationName"
label="Název organizace, kde působíš:"
saveButtonLabel="Uložit organizaci"
placeholder="název firmy, neziskové organizace, státní instituce, …"
defaultValue={model?.organizationName}
disabled={!model || updating}
/>

<InputWithSaveButton
onSave={(profileUrl) => onChange({ ...model!, profileUrl })}
id="professionalProfile"
type="url"
label="Odkaz na tvůj web nebo profesní profil:"
saveButtonLabel="Uložit odkaz"
defaultValue={model?.profileUrl}
disabled={!model || updating}
/>

<DistrictSelect
value={model?.availableInDistricts ?? ""}
onChange={(availableInDistricts) =>
onChange({ ...model!, availableInDistricts })
}
/>
</section>
);

const PrivacySection = ({ model, updating, onChange }: SectionProps) => {
const hasPublicProfile = model?.privacyFlags.includes("enablePublicProfile");

Expand Down Expand Up @@ -214,126 +263,6 @@ const PrivacySection = ({ model, updating, onChange }: SectionProps) => {
);
};

const WorkSection = ({ model, updating, onChange }: SectionProps) => {
const occupationsOptions = {
"private-sector": "Pracuji v soukromém sektoru",
"non-profit": "Pracuji v neziskové organizaci",
"state": "Pracuji ve státním sektoru",
"freelancing": "Jsem na volné noze/freelancer",
"studying": "Studuji",
"parental-leave": "Jsem na rodičovské",
"looking-for-job": "Hledám práci",
"other": "Jiné",
};

const [occupation, setOccupation] = useState("");

useEffect(() => {
setOccupation(model?.occupation ?? "");
}, [model]);

return (
<section className="flex max-w-prose flex-col gap-4">
<h2 className="typo-title2">Práce</h2>

<div className="flex flex-col gap-2">
<label htmlFor="occupation" className="block">
Čemu se aktuálně věnuješ:
</label>
<div>
{Object.entries(occupationsOptions).map(([id, label]) => (
<label key={id} className="mb-1 flex items-center">
<input
type="radio"
className="mr-3"
name="occupation"
checked={occupation == id}
disabled={updating}
onChange={() =>
onChange({
...model!,
occupation: id,
})
}
/>
<span className={occupation === id ? "font-bold" : ""}>
{label}
</span>
</label>
))}
</div>
</div>

<InputWithSaveButton
id="organizationName"
label="Název organizace, kde působíš:"
saveButtonLabel="Uložit organizaci"
placeholder="název firmy, neziskové organizace, státní instituce, …"
defaultValue={model?.organizationName}
disabled={!model || updating}
onSave={(organizationName) => onChange({ ...model!, organizationName })}
/>

<InputWithSaveButton
id="professionalProfile"
type="url"
label="Odkaz na tvůj web nebo profesní profil:"
saveButtonLabel="Uložit odkaz"
defaultValue={model?.profileUrl}
disabled={!model || updating}
onSave={(profileUrl) => onChange({ ...model!, profileUrl })}
/>
</section>
);
};


const SkillSection = ({ model, updating, onChange }: SectionProps) => {
const selection = model ? decodeSkillSelection(model.skills) : {};

// TBD: Batch updates without blocking the UI?
const onSelectionChange = (selection: SkillSelection) => {
onChange({ ...model!, skills: encodeSkillSelection(selection) });
};

return (
<section className="flex flex-col gap-4">
<h2 className="typo-title2">Co umíš?</h2>
<p className="max-w-prose">
Dej nám to vědět, ať ti můžeme různými kanály nabízet relevantnější
příležitosti.
</p>
<SkillPicker
skillMenu={skills}
selection={selection}
onChange={onSelectionChange}
disabled={updating}
/>
</section>
);
};

const MapSection = ({ model, onChange }: SectionProps) => {
return (
<section className="flex max-w-prose flex-col gap-4">
<h2 className="typo-title2">Kde býváš k zastižení?</h2>
<p>
Jsme Česko.Digital, ne Praha.Digital :) Jestli chceš, dej nám vědět, ve
kterých okresech ČR se vyskytuješ, ať můžeme lépe propojit členy a
členky Česko.Digital z různých koutů Česka.
</p>
<div>
<DistrictSelect
value={model?.availableInDistricts ?? ""}
onChange={(availableInDistricts) =>
onChange({ ...model!, availableInDistricts })
}
/>
</div>
</section>
);
};

//
// Shared Code
//
Expand Down
8 changes: 7 additions & 1 deletion app/account/me/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
getUserProfileByMail,
privacyFlags,
updateUserProfile,
userSeniorities,
} from "~/src/data/user-profile";
import { normalizeEmailAddress } from "~/src/utils";

Expand All @@ -24,7 +25,8 @@ export async function POST(request: NextRequest): Promise<Response> {
const decodeRequest = record({
name: string,
email: string,
skills: string,
tags: string,
maxSeniority: optional(union(...userSeniorities)),
gdprPolicyAcceptedAt: string,
codeOfConductAcceptedAt: string,
occupation: optional(string),
Expand Down Expand Up @@ -87,6 +89,8 @@ export async function PATCH(request: NextRequest) {
contactEmail,
availableInDistricts,
bio,
tags,
maxSeniority,
occupation,
organizationName,
profileUrl,
Expand All @@ -99,6 +103,8 @@ export async function PATCH(request: NextRequest) {
contactEmail,
availableInDistricts,
bio,
tags,
maxSeniority,
occupation,
organizationName,
profileUrl,
Expand Down
Loading

0 comments on commit 7909ffa

Please sign in to comment.