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

Commit

Permalink
feat: cannot edit a wish with votes (#80)
Browse files Browse the repository at this point in the history
  • Loading branch information
pixelass authored Aug 23, 2021
1 parent bf8d325 commit 47e17ea
Show file tree
Hide file tree
Showing 20 changed files with 376 additions and 40 deletions.
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

1 comment on commit 47e17ea

@vercel
Copy link

@vercel vercel bot commented on 47e17ea Aug 23, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.