Skip to content
This repository was archived by the owner on May 24, 2022. It is now read-only.

feat: cannot edit a wish with votes #80

Merged
merged 1 commit into from
Aug 23, 2021
Merged
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"@apollo/react-hooks": "4.0.0",
"@contentful/rich-text-react-renderer": "15.1.0",
"@contentful/rich-text-types": "15.1.0",
"@dekk-app/dekk-backend": "1.5.3",
"@dekk-app/dekk-backend": "1.6.1",
"@emotion/cache": "11.4.0",
"@emotion/core": "11.0.0",
"@emotion/react": "11.4.1",
Expand Down
4 changes: 3 additions & 1 deletion public/static/locales/de/form.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
"minLength": "Die Mindestlänge ist {{minLength}}.",
"passwordMatch": "Die Passwörter stimmen nicht überein.",
"pattern": "Überprüfe bitte deine Eingabe.",
"required": "Dieses Feld ist ein Pflichtfeld."
"required": "Dieses Feld ist ein Pflichtfeld.",
"generic-error": "Ein Fehler ist aufgetreten.",
"CANNOT_UPDATE_VOTED_WISH": "Ein Wunsch mit Stimmen kann nicht bearbeitet werden."
},
"fields-labels": {
"confirmPassword": "Passwort bestätigen",
Expand Down
4 changes: 3 additions & 1 deletion public/static/locales/en/form.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
"minLength": "The minimum length is {{minLength}}.",
"passwordMatch": "The passwords do not match.",
"pattern": "Please check your input.",
"required": "This field is required."
"required": "This field is required.",
"generic-error": "An error occurred.",
"CANNOT_UPDATE_VOTED_WISH": "Cannot update a wish that already has votes."
},
"fields-labels": {
"confirmPassword": "Confirm password",
Expand Down
32 changes: 17 additions & 15 deletions src/atoms/error-text/styled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,32 @@ import { pxToRem } from "@/ions/utils/unit";
import { css } from "@emotion/react";
import styled from "@emotion/styled";

export const StyledErrorText = styled.div`
export interface StyledErrorTextProps {
arrow?: boolean;
}
export const StyledErrorText = styled.div<StyledErrorTextProps>`
--tip-size: ${pxToRem(8)};

position: relative;
width: 100%;
margin: ${pxToRem(-6)} 0 ${pxToRem(12)};
padding: ${pxToRem(6)} ${pxToRem(24)};

&::before {
content: "";
position: absolute;
bottom: 100%;
left: calc(var(--tip-size) * 2);
border: var(--tip-size) solid transparent;
border-top: 0;
}

${({ theme }) => css`
${({ theme, arrow }) => css`
border-radius: ${theme.shapes.s};
background: ${theme.ui.atoms.errorText.background};
color: ${theme.ui.atoms.errorText.color};

&::before {
border-bottom-color: ${theme.ui.atoms.errorText.background};
}
${arrow &&
css`
&::before {
content: "";
position: absolute;
bottom: 100%;
left: calc(var(--tip-size) * 2);
border: var(--tip-size) solid transparent;
border-bottom-color: ${theme.ui.atoms.errorText.background};
border-top: 0;
}
`};
`};
`;
7 changes: 5 additions & 2 deletions src/atoms/icon-button/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { HTMLAttributes } from "react";
import { HTMLAttributes, HTMLProps } from "react";
import { Except } from "type-fest";

export interface IconButtonProps extends HTMLAttributes<HTMLButtonElement> {}
export interface IconButtonProps
extends HTMLAttributes<HTMLButtonElement>,
Except<HTMLProps<HTMLButtonElement>, "as" | "type"> {}
9 changes: 7 additions & 2 deletions src/atoms/input-wrapper/styled.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { pxToRem } from "@/ions/utils/unit";
import { css } from "@emotion/react";
import styled from "@emotion/styled";
import { StyledInputWrapperProps } from "./types";

export const StyledInputWrapper = styled.label<{ focused?: boolean; fullWidth?: boolean }>`
export const StyledInputWrapper = styled.label<StyledInputWrapperProps>`
position: relative;
margin: 0 auto ${pxToRem(16)};
padding: 0;
Expand All @@ -19,8 +20,12 @@ export const StyledInputWrapper = styled.label<{ focused?: boolean; fullWidth?:
pointer-events: none;
}

${({ theme, fullWidth }) => css`
${({ theme, fullWidth, disabled }) => css`
width: ${fullWidth ? "100%" : "auto"};
${disabled &&
css`
opacity: 0.5;
`};
&::before {
border-radius: ${theme.shapes.s};
box-shadow: inset 0 0 0 1px currentColor;
Expand Down
5 changes: 5 additions & 0 deletions src/atoms/input-wrapper/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface StyledInputWrapperProps {
focused?: boolean;
fullWidth?: boolean;
disabled?: boolean;
}
9 changes: 8 additions & 1 deletion src/ions/contexts/wishlist/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,21 @@ import { UpdateCallback, WishlistState } from "./types";

export const WishlistContext = createContext<WishlistState>({
wishes: [],
error: null,
add() {
/**/
},
update() {
/**/
},
setError() {
/**/
},
});

export const WishlistProvider: FC<{ initialState: Wish[] }> = ({ children, initialState }) => {
const [wishes, setWishes] = useState<Wish[]>(initialState);
const [error, setError] = useState<string | null>(null);

const add = useCallback((wish: Wish) => {
setWishes(previousState => [wish, ...previousState]);
Expand Down Expand Up @@ -49,8 +54,10 @@ export const WishlistProvider: FC<{ initialState: Wish[] }> = ({ children, initi
wishes,
add,
update,
setError,
error,
}),
[add, wishes, update]
[add, wishes, update, setError, error]
);

return <WishlistContext.Provider value={context}>{children}</WishlistContext.Provider>;
Expand Down
3 changes: 3 additions & 0 deletions src/ions/contexts/wishlist/types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { Wish } from "@/types/backend-api";
import { Dispatch, SetStateAction } from "react";

export type UpdateCallback = (previousWish: Wish) => Partial<Wish>;

export interface WishlistState {
wishes: Wish[];
error: string;
setError: Dispatch<SetStateAction<string>>;
add(wish: Wish): void;
update(id: number, callback: UpdateCallback): void;
}
13 changes: 11 additions & 2 deletions src/molecules/input-field/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ const InputField: FC<InputFieldProps> = ({
fullWidth,
defaultValue,
autoFocus,
readOnly,
disabled,
helpText,
required,
validation = {},
Expand All @@ -41,7 +43,12 @@ const InputField: FC<InputFieldProps> = ({

return (
<>
<StyledInputWrapper fullWidth={fullWidth} focused={focused} htmlFor={`${id}_field`}>
<StyledInputWrapper
fullWidth={fullWidth}
disabled={disabled}
focused={focused}
htmlFor={`${id}_field`}
>
<StyledFloatingLabel
floating={focused || filled || isValid}
initial={isValid}
Expand All @@ -58,6 +65,8 @@ const InputField: FC<InputFieldProps> = ({
id={`${id}_field`}
name={name}
autoFocus={autoFocus}
readOnly={readOnly}
disabled={disabled}
required={Boolean(validation.required)}
invalid={Boolean(errors[name])}
type={type}
Expand All @@ -83,7 +92,7 @@ const InputField: FC<InputFieldProps> = ({
</StyledInputWrapper>

{errors[name] ? (
<StyledErrorText>
<StyledErrorText arrow>
<Typography raw id={`${id}_help`}>
{t(`form:errors.${(errors[name] as FieldError).type as string}`, {
minLength: 2,
Expand Down
50 changes: 50 additions & 0 deletions src/molecules/snackbar/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import Icon from "@/atoms/icon";
import { useTranslation } from "next-i18next";
import React, { FC, useEffect } from "react";
import { StyledSnackbar, StyledSnackbarButton, StyledSnackbarMessage } from "./styled";
import { SnackbarProps } from "./types";

const Snackbar: FC<SnackbarProps> = ({
children,
id,
level = "default",
autoClose = 6000,
onClose,
fixed,
...props
}) => {
const { t } = useTranslation(["common"]);
useEffect(() => {
if (onClose && autoClose > 0) {
const timer = setTimeout(() => {
onClose();
}, autoClose);
return () => {
onClose();
clearTimeout(timer);
};
}

return () => {
/**/
};
}, [onClose, autoClose]);
return (
<StyledSnackbar
{...props}
fixed={fixed}
level={level}
role="alertdialog"
aria-describedby={id}
>
<StyledSnackbarMessage id={id}>{children}</StyledSnackbarMessage>
{onClose && (
<StyledSnackbarButton autoFocus aria-label={t("common:close")} onClick={onClose}>
<Icon icon="close" />
</StyledSnackbarButton>
)}
</StyledSnackbar>
);
};

export default Snackbar;
53 changes: 53 additions & 0 deletions src/molecules/snackbar/styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import IconButton from "@/atoms/icon-button";
import { shadows } from "@/ions/theme";
import { pxToRem } from "@/ions/utils/unit";
import { css } from "@emotion/react";
import styled from "@emotion/styled";
import { SnackbarBackgrounds, SnackbarColors, StyledSnackbarProps } from "./types";

export const StyledSnackbar = styled.div<StyledSnackbarProps>`
display: grid;
grid-template-columns: 1fr auto;
width: ${pxToRem(600)};
${({ theme, level, fixed }) => css`
${fixed &&
css`
position: fixed;
z-index: 5;
bottom: 0;
left: 50%;
transform: translateX(-50%);
box-shadow: ${shadows.l};
`};
grid-gap: ${pxToRem(theme.spaces.xs)};
max-width: ${fixed ? `calc(100% - ${pxToRem(2 * theme.spaces.l)})` : "100%"};
margin: ${pxToRem(theme.spaces.m)} 0;
padding: ${pxToRem(theme.spaces.s)} ${pxToRem(theme.spaces.m)};
border-radius: ${theme.shapes.s};
background: ${theme.palette[backgroundColors[level]]};
color: ${colors[level]};
`};
`;

const backgroundColors: SnackbarBackgrounds = {
default: "dark",
warning: "yellow",
info: "blue",
error: "red",
};

const colors: SnackbarColors = {
default: "white",
warning: "black",
info: "black",
error: "white",
};

export const StyledSnackbarButton = styled(IconButton)`
align-self: center;
${({ theme }) => css`
margin: ${pxToRem(-theme.spaces.s)} ${pxToRem(-theme.spaces.xs)};
`};
`;

export const StyledSnackbarMessage = styled.div``;
20 changes: 20 additions & 0 deletions src/molecules/snackbar/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Palette } from "@/types/theme";

export type SnackbarLevel = "warning" | "info" | "error" | "default";

export type SnackbarBackgrounds = Record<SnackbarLevel, keyof Palette>;

export type SnackbarColor = "black" | "white";

export type SnackbarColors = Record<SnackbarLevel, SnackbarColor>;

export interface StyledSnackbarProps {
level?: SnackbarLevel;
fixed?: boolean;
}

export interface SnackbarProps extends StyledSnackbarProps {
autoClose?: number;
id: string;
onClose?(): void;
}
13 changes: 11 additions & 2 deletions src/molecules/textarea-field/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ const TextArea: FC<TextAreaFieldProps> = ({
fullWidth,
defaultValue,
autoFocus,
readOnly,
disabled,
helpText,
required,
validation = {},
Expand Down Expand Up @@ -57,7 +59,12 @@ const TextArea: FC<TextAreaFieldProps> = ({

return (
<>
<StyledInputWrapper fullWidth={fullWidth} focused={focused} htmlFor={`${id}_field`}>
<StyledInputWrapper
fullWidth={fullWidth}
disabled={disabled}
focused={focused}
htmlFor={`${id}_field`}
>
<StyledFloatingLabel
floating={focused || filled || isValid}
initial={isValid}
Expand All @@ -71,6 +78,8 @@ const TextArea: FC<TextAreaFieldProps> = ({
id={`${id}_field`}
name={name}
autoFocus={autoFocus}
readOnly={readOnly}
disabled={disabled}
required={Boolean(validation.required)}
invalid={Boolean(errors[name])}
data-test-id={testId}
Expand All @@ -95,7 +104,7 @@ const TextArea: FC<TextAreaFieldProps> = ({
</StyledInputWrapper>

{errors[name] ? (
<StyledErrorText>
<StyledErrorText arrow>
<Typography raw id={`${id}_help`}>
{t(`form:errors.${(errors[name] as FieldError).type as string}`, {
minLength: 2,
Expand Down
Loading