diff --git a/README.md b/README.md
index 89dd1ab15..4db3360d4 100644
--- a/README.md
+++ b/README.md
@@ -36,6 +36,7 @@ cd infra/dev
```
make build
+make up
```
Once this is completed you should have:
diff --git a/front/src/components/form/__stories__/Checkbox.stories.tsx b/front/src/components/form/__stories__/Checkbox.stories.tsx
index 903095f62..4d2bdc376 100644
--- a/front/src/components/form/__stories__/Checkbox.stories.tsx
+++ b/front/src/components/form/__stories__/Checkbox.stories.tsx
@@ -1,5 +1,3 @@
-import { MemoryRouter } from 'react-router-dom';
-
import Checkbox from '../Checkbox';
import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../layout/styles/themes';
@@ -12,9 +10,7 @@ export default {
export const RegularCheckbox = () => {
return (
-
-
-
+
);
};
diff --git a/front/src/components/form/__tests__/Checkbox.test.tsx b/front/src/components/form/__tests__/Checkbox.test.tsx
index 3e0180ad3..b4b2c402e 100644
--- a/front/src/components/form/__tests__/Checkbox.test.tsx
+++ b/front/src/components/form/__tests__/Checkbox.test.tsx
@@ -2,7 +2,7 @@ import { render } from '@testing-library/react';
import { RegularCheckbox } from '../__stories__/Checkbox.stories';
-it('Checks the NavItem renders', () => {
+it('Checks the Checkbox renders', () => {
const { getByTestId } = render();
expect(getByTestId('input-checkbox')).toHaveAttribute(
diff --git a/front/src/components/table/Table.tsx b/front/src/components/table/Table.tsx
index 844e21ad8..2e7624f0a 100644
--- a/front/src/components/table/Table.tsx
+++ b/front/src/components/table/Table.tsx
@@ -6,7 +6,7 @@ import {
getCoreRowModel,
useReactTable,
} from '@tanstack/react-table';
-import TableHeader from './TableHeader';
+import TableHeader from './table-header/TableHeader';
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import styled from '@emotion/styled';
diff --git a/front/src/components/table/table-header/DropdownButton.tsx b/front/src/components/table/table-header/DropdownButton.tsx
new file mode 100644
index 000000000..6aac67046
--- /dev/null
+++ b/front/src/components/table/table-header/DropdownButton.tsx
@@ -0,0 +1,106 @@
+import styled from '@emotion/styled';
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { useState, useRef } from 'react';
+import { useOutsideAlerter } from '../../../hooks/useOutsideAlerter';
+import { modalBackground } from '../../../layout/styles/themes';
+
+type OwnProps = {
+ label: string;
+ options: Array<{ label: string; icon: IconProp }>;
+};
+
+const StyledDropdownButtonContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ position: relative;
+`;
+
+type StyledDropdownButtonProps = {
+ isUnfolded: boolean;
+};
+
+const StyledDropdownButton = styled.div`
+ display: flex;
+ margin-left: ${(props) => props.theme.spacing(3)};
+ cursor: pointer;
+ background: ${(props) => props.theme.primaryBackground};
+ padding: ${(props) => props.theme.spacing(1)};
+ border-radius: 4px;
+ filter: ${(props) => (props.isUnfolded ? 'brightness(0.95)' : 'none')};
+
+ &:hover {
+ filter: brightness(0.95);
+ }
+`;
+
+const StyledDropdown = styled.ul`
+ display: flex;
+ position: absolute;
+ top: 14px;
+ right: 0;
+ border: 1px solid ${(props) => props.theme.primaryBorder};
+ box-shadow: 0px 3px 12px rgba(0, 0, 0, 0.09);
+ border-radius: 8px;
+ padding: 0px;
+ min-width: 160px;
+ ${modalBackground}
+`;
+
+const StyledDropdownItem = styled.li`
+ display: flex;
+ padding: ${(props) => props.theme.spacing(2)}
+ calc(${(props) => props.theme.spacing(2)} - 2px);
+ margin: 2px;
+ background: ${(props) => props.theme.primaryBackground};
+ cursor: pointer;
+ width: 100%;
+ border-radius: 4px;
+ color: ${(props) => props.theme.text60};
+
+ &:hover {
+ filter: brightness(0.95);
+ }
+`;
+
+const StyledIcon = styled.div`
+ display: flex;
+ margin-right: ${(props) => props.theme.spacing(1)};
+`;
+
+function DropdownButton({ label, options }: OwnProps) {
+ const [isUnfolded, setIsUnfolded] = useState(false);
+
+ const onButtonClick = () => {
+ setIsUnfolded(!isUnfolded);
+ };
+
+ const onOutsideClick = () => {
+ setIsUnfolded(false);
+ };
+
+ const dropdownRef = useRef(null);
+ useOutsideAlerter(dropdownRef, onOutsideClick);
+
+ return (
+
+
+ {label}
+
+ {isUnfolded && options.length > 0 && (
+
+ {options.map((option, index) => (
+
+
+
+
+ {option.label}
+
+ ))}
+
+ )}
+
+ );
+}
+
+export default DropdownButton;
diff --git a/front/src/components/table/TableHeader.tsx b/front/src/components/table/table-header/TableHeader.tsx
similarity index 73%
rename from front/src/components/table/TableHeader.tsx
rename to front/src/components/table/table-header/TableHeader.tsx
index 7d26e6296..ac1f3c5f6 100644
--- a/front/src/components/table/TableHeader.tsx
+++ b/front/src/components/table/table-header/TableHeader.tsx
@@ -1,6 +1,8 @@
import styled from '@emotion/styled';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import DropdownButton from './DropdownButton';
import { IconProp } from '@fortawesome/fontawesome-svg-core';
+import { faCalendar } from '@fortawesome/pro-regular-svg-icons';
type OwnProps = {
viewName: string;
@@ -30,12 +32,7 @@ const StyledViewSection = styled.div`
const StyledFilters = styled.div`
display: flex;
font-weight: 400;
- margin-right: ${(props) => props.theme.spacing(1)};
-`;
-
-const StyledFilterButton = styled.div`
- display: flex;
- margin-left: ${(props) => props.theme.spacing(4)};
+ margin-right: ${(props) => props.theme.spacing(2)};
`;
function TableHeader({ viewName, viewIcon }: OwnProps) {
@@ -48,9 +45,12 @@ function TableHeader({ viewName, viewIcon }: OwnProps) {
{viewName}
- Filter
- Sort
- Settings
+
+
+
);
diff --git a/front/src/components/table/table-header/__stories__/TableHeader.stories.tsx b/front/src/components/table/table-header/__stories__/TableHeader.stories.tsx
new file mode 100644
index 000000000..dc804ba31
--- /dev/null
+++ b/front/src/components/table/table-header/__stories__/TableHeader.stories.tsx
@@ -0,0 +1,17 @@
+import TableHeader from '../TableHeader';
+import { ThemeProvider } from '@emotion/react';
+import { lightTheme } from '../../../../layout/styles/themes';
+import { faBuilding } from '@fortawesome/pro-regular-svg-icons';
+
+export default {
+ title: 'TableHeader',
+ component: TableHeader,
+};
+
+export const RegularTableHeader = () => {
+ return (
+
+
+
+ );
+};
diff --git a/front/src/components/table/table-header/__tests__/TableHeader.test.tsx b/front/src/components/table/table-header/__tests__/TableHeader.test.tsx
new file mode 100644
index 000000000..d3d7fe83b
--- /dev/null
+++ b/front/src/components/table/table-header/__tests__/TableHeader.test.tsx
@@ -0,0 +1,9 @@
+import { render } from '@testing-library/react';
+
+import { RegularTableHeader } from '../__stories__/TableHeader.stories';
+
+it('Checks the TableHeader renders', () => {
+ const { getByText } = render();
+
+ expect(getByText('Test')).toBeDefined();
+});
diff --git a/front/src/hooks/__tests__/useOutsideAlerter.test.tsx b/front/src/hooks/__tests__/useOutsideAlerter.test.tsx
new file mode 100644
index 000000000..e9aa33644
--- /dev/null
+++ b/front/src/hooks/__tests__/useOutsideAlerter.test.tsx
@@ -0,0 +1,33 @@
+const onOutsideClick = jest.fn();
+import { useRef } from 'react';
+import TableHeader from '../../components/table/table-header/TableHeader';
+import { render, fireEvent } from '@testing-library/react';
+import { useOutsideAlerter } from '../useOutsideAlerter';
+import { act } from 'react-dom/test-utils';
+
+function TestComponent() {
+ const buttonRef = useRef(null);
+ useOutsideAlerter(buttonRef, onOutsideClick);
+
+ return (
+
+ Outside
+
+
+ );
+}
+
+export default TableHeader;
+
+test('clicking the button toggles an answer on/off', async () => {
+ const { getByText } = render();
+ const inside = getByText('Inside');
+ const outside = getByText('Outside');
+ await act(() => Promise.resolve());
+
+ fireEvent.mouseDown(inside);
+ expect(onOutsideClick).toHaveBeenCalledTimes(0);
+
+ fireEvent.mouseDown(outside);
+ expect(onOutsideClick).toHaveBeenCalledTimes(1);
+});
diff --git a/front/src/hooks/useOutsideAlerter.ts b/front/src/hooks/useOutsideAlerter.ts
new file mode 100644
index 000000000..746c44afb
--- /dev/null
+++ b/front/src/hooks/useOutsideAlerter.ts
@@ -0,0 +1,23 @@
+import { useEffect } from 'react';
+
+declare type CallbackType = () => void;
+
+export function useOutsideAlerter(
+ ref: React.RefObject,
+ callback: CallbackType,
+) {
+ useEffect(() => {
+ function handleClickOutside(event: Event) {
+ console.log('test3');
+
+ const target = event.target as HTMLButtonElement;
+ if (ref.current && !ref.current.contains(target)) {
+ callback();
+ }
+ }
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, [ref]);
+}
diff --git a/front/src/layout/styles/themes.ts b/front/src/layout/styles/themes.ts
index 160f14d63..74eee0bda 100644
--- a/front/src/layout/styles/themes.ts
+++ b/front/src/layout/styles/themes.ts
@@ -1,3 +1,5 @@
+import { css } from '@emotion/react';
+
const commonTheme = {
fontSizeSmall: '0.92rem',
fontSizeMedium: '1rem',
@@ -21,6 +23,8 @@ const lightThemeSpecific = {
purpleBackground: '#e0e0ff',
yellowBackground: '#fff2e7',
+ secondaryBackgroundSmallTransparency: 'rgba(252, 252, 252, 0.8)',
+
primaryBorder: 'rgba(0, 0, 0, 0.08)',
text100: '#000',
@@ -49,6 +53,10 @@ const darkThemeSpecific = {
purpleBackground: '#1111b7',
yellowBackground: '#cc660a',
+ secondaryBackgroundSmallTransparency: 'rgba(23, 23, 23, 0.8)',
+
+ primaryBorder: 'rgba(255, 255, 255, 0.08)',
+
text100: '#ffffff',
text80: '#ccc',
text60: '#999',
@@ -64,6 +72,12 @@ const darkThemeSpecific = {
yellow: '#fff2e7',
};
+export const modalBackground = (props: any) =>
+ css`
+ backdrop-filter: blur(20px);
+ background: ${props.theme.secondaryBackgroundSmallTransparency};
+ `;
+
export const lightTheme = { ...commonTheme, ...lightThemeSpecific };
export const darkTheme = { ...commonTheme, ...darkThemeSpecific };