Skip to content

Commit

Permalink
[CPF-144] Project scope - select bucket (#151)
Browse files Browse the repository at this point in the history
* feat: fetch data for select bucket - project scope step

* feat: select bucket - project scope step

* feat: fix profile notification checkboxes
  • Loading branch information
r1skz3ro authored Sep 11, 2024
1 parent dc928c6 commit b8f7cd0
Show file tree
Hide file tree
Showing 31 changed files with 407 additions and 98 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,81 @@
export default function Score() {
return <div>Project Scope Page</div>;
import { ProjectScope } from '@app/components/pages/mySpace/addProject/ProjectScope';
import { BandWithBuckets } from '@app/types/library';
import { UserLadder } from '@app/types/user';
import { mapKeysToCamelCase } from '@app/utils';
import { createClient } from '@app/utils/supabase/server';
import { redirect } from 'next/navigation';

export default async function Score() {
const supabase = createClient();
const {
data: { user },
error,
} = await supabase.auth.getUser();

if (error || !user) {
redirect('/auth');
}

const { data: ladders } = await supabase
.from('user_ladder')
.select(
`
ladder(
ladder_slug
),
is_main_ladder
`,
)
.eq('user_id', user.id);

const laddersData = mapKeysToCamelCase<UserLadder[]>(ladders);

if (!laddersData) {
return <div>No ladders assigned to a user</div>;
}

const mainLadder = laddersData?.length > 1 ? laddersData.find((ladder) => ladder.isMainLadder) : laddersData[0];

const { data: bands } = await supabase
.from('band')
.select(
`
band_id,
ladder_slug,
threshold,
salary_range,
band_number,
buckets:bucket(
bucket_slug,
bucket_name,
description,
bucket_type,
advancement_levels:advancement_level(
advancement_level,
description,
bucket_slug,
skills:atomic_skill(
skill_id,
name,
description,
category
),
projects:example_project(
project_id,
level_id,
title,
overview
)
)
),
ladder:ladder_slug(
ladder_name
)
`,
)
.eq('ladder_slug', mainLadder?.ladder?.ladderSlug);

const bandsData = mapKeysToCamelCase<BandWithBuckets[]>(bands);

return <ProjectScope bands={bandsData} />;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { AtomicSkill } from '@app/types/library';

export interface AccordionCardProps extends React.HTMLAttributes<HTMLDivElement> {
title: string;
expandedByDefault?: boolean;
small?: boolean;
checkboxName?: string;
handleSelectAll?: (name: string, selected: boolean, skills?: AtomicSkill[]) => void;
}
26 changes: 24 additions & 2 deletions frontend/src/components/common/AccordionCard/AccordionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ import { ChevronRightIcon } from '@app/static/icons/ChevronRightIcon';
import { generateClassNames } from '@app/utils';
import { AccordionCardProps } from './AccordionCard.interface';
import { Typography } from '@app/components/common/Typography';
import { Checkbox } from '../Checkbox';
import { addProjectFormNames } from '@app/components/pages/mySpace/addProject/AddProjectFormProvider/AddProjectFormProvider.interface';

export const AccordionCard = ({
title,
children,
expandedByDefault,
className,
small,
checkboxName,
handleSelectAll,
}: PropsWithChildren<AccordionCardProps>) => {
const [isOpen, setOpen] = useState(false);

Expand All @@ -32,7 +36,21 @@ export const AccordionCard = ({
)}
onClick={() => setOpen(!isOpen)}
>
<Typography variant={small ? 'body-m/semibold' : 'head-s/semibold'}>{title}</Typography>
<div className="flex flex-col items-start gap-y-8">
<Typography variant={small ? 'body-m/semibold' : 'head-s/semibold'}>{title}</Typography>
{checkboxName && handleSelectAll && (
<div className="flex gap-x-4">
<Checkbox
handleChange={(_, selected) => handleSelectAll(checkboxName, selected)}
name={`${checkboxName}.all` as typeof addProjectFormNames.skills}
id="all"
/>
<Typography variant="body-m/semibold" className="text-navy-700">
Select all
</Typography>
</div>
)}
</div>
{children ? (
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-navy-50 group-hover:bg-white">
<ChevronRightIcon
Expand All @@ -42,7 +60,11 @@ export const AccordionCard = ({
) : undefined}
</button>
{children ? (
<div className={generateClassNames('rounded-b-2xl border border-t-0 border-navy-200 p-6', { hidden: !isOpen })}>
<div
className={generateClassNames('rounded-b-2xl border border-t-0 border-navy-200 px-8 py-4', {
hidden: !isOpen,
})}
>
{children}
</div>
) : undefined}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { ReactNode } from 'react';

export interface AccordionListProps {
checkboxName?: string;
items: {
key: string;
key: string | number;
title: string;
children?: ReactNode;
icon?: ReactNode;
Expand Down
23 changes: 12 additions & 11 deletions frontend/src/components/common/AccordionList/AccordionList.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import { PropsWithChildren } from 'react';
import { AccordionListProps } from './AccordionList.interface';
import { AccordionListItem } from './AccordionListItem';
import { Checkbox } from '../Checkbox';

export const AccordionList = ({ items }: PropsWithChildren<AccordionListProps>) => {
export const AccordionList = ({ items, checkboxName }: PropsWithChildren<AccordionListProps>) => {
return (
<div className="flex flex-col">
{items.map(({ title, children, key, icon }) => (
<AccordionListItem
key={key}
title={title}
noContentTooltipText="There's no description for this skill."
icon={icon}
>
{children}
</AccordionListItem>
))}
{items.map(({ title, children, key, icon }) => {
return (
<div key={key} className="flex items-center">
{checkboxName && <Checkbox name={`${checkboxName}.${key}`} id={key.toString()} />}
<AccordionListItem title={title} noContentTooltipText="There's no description for this skill." icon={icon}>
{children}
</AccordionListItem>
</div>
);
})}
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const AccordionListItem = ({
};

return (
<div className="border-b border-b-navy-200 px-2 py-4">
<div className="w-full border-b border-b-navy-200 p-4">
<button
className={generateClassNames('flex w-full items-center justify-between', {
'cursor-pointer': children,
Expand All @@ -29,7 +29,7 @@ export const AccordionListItem = ({
>
<div className="flex items-center gap-4">
{icon}
<Typography variant="body-m/medium" className="text-left text-navy-600">
<Typography variant="body-m/medium" className="text-left text-navy-700">
{title}
</Typography>
</div>
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/components/common/Checkbox/Checkbox.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface CheckboxProps {
name: string;
id?: string;
checked?: boolean;
disabled?: boolean;
defaultChecked?: boolean;
handleChange?: (name: string, selected: boolean) => void;
}
31 changes: 25 additions & 6 deletions frontend/src/components/common/Checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { FC } from 'react';
import { Checkbox as HeadlessCheckbox, CheckboxProps } from '@headlessui/react';
import { Checkbox as HeadlessCheckbox } from '@headlessui/react';
import { generateClassNames } from '@app/utils';
import { useController, useFormContext } from 'react-hook-form';
import { CheckboxProps } from './Checkbox.interface';

const styles: { [checked: string]: { [disabled: string]: string } } = {
true: {
Expand All @@ -14,15 +16,32 @@ const styles: { [checked: string]: { [disabled: string]: string } } = {
};

export const Checkbox: FC<CheckboxProps> = (props) => {
const { disabled, checked } = props;
const currentStyles = styles[(checked ?? false).toString()][(disabled ?? false).toString()];
const { name, id, disabled, handleChange, checked = false } = props;
const form = useFormContext();
const { field } = useController({ name, control: form.control });
const selected = field?.value?.selected;

const currentStyles = styles[(selected ?? false).toString()][(disabled ?? false).toString()];

return (
<HeadlessCheckbox
className={generateClassNames('flex h-6 w-6 items-center justify-center rounded', currentStyles)}
{...props}
className={generateClassNames('flex h-6 w-6 cursor-pointer items-center justify-center rounded', currentStyles)}
{...field}
onChange={(selected) => {
if (handleChange) {
handleChange(name, selected);
}

field.onChange({
id: id,
selected: selected,
});
}}
checked={selected ?? checked}
onClick={(e) => e.stopPropagation()}
value={selected ?? checked}
>
{checked && (
{selected && (
<svg width="16" height="11" viewBox="0 0 16 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
Expand Down
25 changes: 16 additions & 9 deletions frontend/src/components/common/Combobox/Combobox.hooks.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
import { useMemo, useState } from 'react';
import { ComboboxProps } from './Combobox.interface';

export const useCombobox = (options: ComboboxProps['options'], selectedOptions: ComboboxProps['selectedOptions']) => {
export const useCombobox = (
options: ComboboxProps['options'],
selectedOptions: ComboboxProps['selectedOptions'],
sort: ComboboxProps['sort'],
) => {
const [query, setQuery] = useState('');

const uniqOptions =
Array.isArray(selectedOptions) && selectedOptions.length > 0
? options.filter((option) => !selectedOptions?.find((selectedOption) => selectedOption?.id === option.id))
: options;

const filteredOptions = useMemo(
() =>
uniqOptions
.sort((a, b) => a.name.toLocaleLowerCase().localeCompare(b.name.toLocaleLowerCase()))
.filter((option) => option.name.toLowerCase().includes(query.toLowerCase())),
[uniqOptions, query],
);
const filteredOptions = useMemo(() => {
if (uniqOptions) {
const filtered = uniqOptions.filter((option) => option.name.toLowerCase().includes(query.toLowerCase()));
if (sort) {
return filtered.sort((a, b) => a.name.toLocaleLowerCase().localeCompare(b.name.toLocaleLowerCase()));
}
return filtered;
}
return uniqOptions;
}, [uniqOptions, sort, query]);

return { filteredOptions, setQuery };
return { filteredOptions: filteredOptions, setQuery };
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export interface ComboboxProps {
name: string;
renderRightContent?: () => ReactNode;
className?: ClassName;
sort?: boolean;
}
4 changes: 3 additions & 1 deletion frontend/src/components/common/Combobox/Combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ export const Combobox: React.FC<ComboboxProps> = ({
renderRightContent,
className,
selectedOptions,
sort = true,
}) => {
const { control } = useFormContext();
const { setQuery, filteredOptions } = useCombobox(options, selectedOptions);
const { setQuery, filteredOptions } = useCombobox(options, selectedOptions, sort);

return (
<Controller
Expand Down Expand Up @@ -61,6 +62,7 @@ export const Combobox: React.FC<ComboboxProps> = ({
<ComboboxOption
key={option.id}
value={option}
disabled={option.available === false}
className="relative cursor-default select-none py-2 pl-3 pr-9 text-navy-900 data-[focus]:cursor-pointer data-[focus]:bg-navy-200 data-[focus]:font-medium"
>
<span className="block truncate">{option.name}</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ export const ExpandableSection: FC<PropsWithChildren<ExpandableSectionProps>> =
</div>
<div className="mb-4 ml-4 flex w-full flex-col gap-4">
<button
className={`flex w-full cursor-pointer flex-col gap-4 rounded-lg ${!open && 'hover:bg-navy-50'}`}
className={`flex w-full cursor-pointer flex-col gap-4 rounded-lg p-4 ${!open && 'hover:bg-navy-50'}`}
onClick={onClick}
>
<Typography as="h3" variant="body-l/semibold">
{title}
</Typography>
{description && (
<Typography variant="body-m/regular" className="text-navy-600">
<Typography variant="body-m/regular" className="text-left text-navy-600">
{description}
</Typography>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const WorkflowTopbar: FC<WorkflowTopbarProps> = ({
activateButtonValue,
}) => {
return (
<div className="sticky top-0 flex h-16 items-center justify-between border-b border-b-navy-200 bg-white px-8">
<div className="sticky top-0 z-10 flex h-16 items-center justify-between border-b border-b-navy-200 bg-white px-8">
<div className="text-l font-semibold text-navy-900">{title}</div>
<div className="flex gap-x-4">
<Button variant="borderless" styleType="natural" onClick={onCancel}>
Expand Down
Loading

0 comments on commit b8f7cd0

Please sign in to comment.