Skip to content

Commit

Permalink
Merge pull request #46 from neko113/feat/#45/comment-notification
Browse files Browse the repository at this point in the history
Feat/#45/comment notification
  • Loading branch information
alstn113 authored Mar 10, 2023
2 parents d9e070b + 5b7e7a9 commit 911613d
Show file tree
Hide file tree
Showing 56 changed files with 2,060 additions and 1,129 deletions.
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v18.14.2
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,17 @@ Emotions는 자신의 감정을 마크다운 형식으로 기록할 수 있는

5. Nest.js에서 class-transformer, class-validator, plainToInstance를 통해서 dto를 요청과 응답에서 알맞는 타입의 값을 받을 수 있게 했습니다.

6. 댓글을 2단계까지 구성하였고, 그 이후로는 mention으로 처리하였습니다.
또한 parentId값을 통해서 한 번의 query 요청으로 댓글들을 받아오고,
이후 Map을 통해 group화하는 방식을 사용했습니다.
6. 댓글을 2단계까지 구성하였고, 그 이후로는 mention으로 처리하였습니다. 또한 parentId값을 통해서 한 번의 query 요청으로 댓글들을 받아오고, 이후 Map을 통해 group화하는 방식을 사용했습니다.

7. AWS SES(Simple Email Service)를 통해서 댓글이나 답글이 달릴 시, 경우에 따라 Post Author이나 Commenter, Mention User에게 Notification Email을 보냅니다.

## `Sturcture`

- Deploy: AWS EC2 Ubuntu, NGINX, Docker, PM2, Git Actions
- DB: Superbase + Postgresql, AWS S3
- Client: React + Vite
- Server: Nest
- Client: React + Vite
- Deploy: AWS EC2 Ubuntu, NGINX, Docker, PM2, Git Actions
- DB: Superbase + Postgresql
- ETC: AWS S3, AWS SES, Google Analytics

## `Website Link`

Expand All @@ -42,8 +43,8 @@ Emotions는 자신의 감정을 마크다운 형식으로 기록할 수 있는
- Postgresql
- Passport
- JWT + Cookie
- AWS S3, AWS SES
- PM2
- AWS

- Frontend
- React
Expand Down
10 changes: 9 additions & 1 deletion packages/client/src/GlobalStyle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,17 @@ export const GlobalStyle = () => {
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Pretendard';
src: url('https://cdn.jsdelivr.net/gh/orioncactus/[email protected]/dist/web/static/pretendard.css');
font-weight: normal;
font-style: normal;
}
body {
font-family: 'Roboto', sans-serif;
font-family: 'Pretendard', sans-serif;
}
// 모바일에서 tap highlight 제거
// transparent로 하면 안됨 (투명하게 보이는 경우가 있음)
* {
Expand Down
124 changes: 124 additions & 0 deletions packages/client/src/components/common/Toggle/Toggle.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { NormalColorType, palette } from '~/lib/styles';
import styled from '@emotion/styled';
import { css } from '@emotion/react';

export const ToggleLabel = styled.label<{ variant: 'sm' | 'lg' }>`
display: flex;
align-items: center;
${({ variant }) =>
variant === 'sm' &&
css`
gap: 0.5rem;
`}
${({ variant }) =>
variant === 'lg' &&
css`
gap: 1rem;
`}
`;

export const ToggleSwitch = styled.span<{ variant: 'sm' | 'lg' }>`
position: relative;
display: flex;
align-items: center;
background-color: ${palette.gray};
cursor: pointer;
transition: background-color 0.2s ease;
${({ variant }) =>
variant === 'sm' &&
css`
width: 2.5rem;
height: 1.5rem;
border-radius: 1rem;
`}
${({ variant }) =>
variant === 'lg' &&
css`
width: 5rem;
height: 3rem;
border-radius: 2rem;
`}
&::after {
content: '';
position: absolute;
display: inline-block;
border-radius: 50%;
background-color: ${palette.white};
transition: transform 0.2s ease, background-color 0.2s ease;
${({ variant }) =>
variant === 'sm' &&
css`
width: 1rem;
height: 1rem;
`}
${({ variant }) =>
variant === 'lg' &&
css`
width: 2rem;
height: 2rem;
`}
}
`;

export const ToggleText = styled.span<{ variant: 'sm' | 'lg' }>`
user-select: none;
${({ variant }) =>
variant === 'sm' &&
css`
font-size: 1rem;
line-height: 1rem;
`}
${({ variant }) =>
variant === 'lg' &&
css`
font-size: 2rem;
line-height: 2rem;
`}
`;

export const ToggleCheckbox = styled.input<{
color: NormalColorType;
variant: 'sm' | 'lg';
}>`
display: none;
// Switch Off
& ~ ${ToggleSwitch} {
&::after {
${({ variant }) =>
variant === 'sm' &&
css`
transform: translateX(0.3rem);
`}
${({ variant }) =>
variant === 'lg' &&
css`
transform: translateX(0.6rem);
`}
}
}
// Switch On
&:checked {
& ~ ${ToggleSwitch} {
background-color: ${({ color }) => palette[color]};
&::after {
${({ variant }) =>
variant === 'sm' &&
css`
transform: translateX(1.2rem);
`}
${({ variant }) =>
variant === 'lg' &&
css`
transform: translateX(2.4rem);
`}
}
}
}
`;
48 changes: 48 additions & 0 deletions packages/client/src/components/common/Toggle/Toggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { forwardRef, InputHTMLAttributes } from 'react';
import { NormalColorType } from '~/lib/styles';
import * as S from './Toggle.styles';

export interface ToggleProps extends InputHTMLAttributes<HTMLInputElement> {
labelText?: string;
color?: NormalColorType;
variant?: 'sm' | 'lg';
}

/**
* @example
* ```tsx
* <Toggle labelText="Toggle" color="primary" />
* <Toggle labelText="Toggle" color="success" />
* <Toggle labelText="Toggle" color="secondary" />
* <Toggle labelText="Toggle" color="warning" />
* <Toggle labelText="Toggle" color="error" />
* ```
* @example
* ```tsx
* <Toggle labelText="Toggle" color="primary" variant="lg" />
* <Toggle labelText="Toggle" color="success" variant="lg" />
* <Toggle labelText="Toggle" color="secondary" variant="lg" />
* <Toggle labelText="Toggle" color="warning" variant="lg" />
* <Toggle labelText="Toggle" color="error" variant="lg" />
* ```
*/
const Toggle = forwardRef<HTMLInputElement, ToggleProps>(function Toggle(
{ labelText = '', color = 'primary', variant = 'sm', ...options },
ref,
) {
return (
<S.ToggleLabel variant={variant}>
<S.ToggleText variant={variant}>{labelText}</S.ToggleText>
<S.ToggleCheckbox
type="checkbox"
variant={variant}
ref={ref}
color={color}
{...options}
/>
<S.ToggleSwitch variant={variant} />
</S.ToggleLabel>
);
});

export default Toggle;
1 change: 1 addition & 0 deletions packages/client/src/components/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export { default as LoadingSpinner } from './LoadingSpinner/LoadingSpinner';
export { default as Modal } from './Modal/Modal';
export { default as Avatar } from './Avatar/Avatar';
export { default as ProgressBar } from './ProgressBar/ProgressBar';
export { default as Toggle } from './Toggle/Toggle';
86 changes: 86 additions & 0 deletions packages/client/src/components/setting/EmailEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import styled from '@emotion/styled';
import { useEffect, useRef } from 'react';
import { mediaQuery } from '~/lib/styles';
import { Button } from '../common';

interface Props {
isEmailEdit: boolean;
nextEmail: string;
onChangeNextEmail: (e: React.ChangeEvent<HTMLInputElement>) => void;
onEdit: () => void;
onCancel: () => void;
}

const EmailEditor = ({
isEmailEdit,
nextEmail,
onChangeNextEmail,
onEdit,
onCancel,
}: Props) => {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (isEmailEdit) {
inputRef.current?.focus();
}
}, [isEmailEdit]);

return (
<Container>
<Input
ref={inputRef}
placeholder="Write Email..."
onChange={onChangeNextEmail}
value={nextEmail}
/>
<ButtonsWrapper>
<Button size="sm" color="success" shadow onClick={onEdit}>
Confirm
</Button>
<Button size="sm" color="error" shadow onClick={onCancel}>
Cancel
</Button>
</ButtonsWrapper>
</Container>
);
};

const Container = styled.div`
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
width: 100%;
gap: 0.5rem;
${mediaQuery.mobile} {
flex-direction: row;
}
`;

const ButtonsWrapper = styled.div`
display: flex;
gap: 0.5rem;
width: 100%;
align-items: center;
justify-content: flex-end;
${mediaQuery.mobile} {
width: auto;
justify-content: center;
}
`;

const Input = styled.input`
width: 100%;
height: 100%;
padding: 0.5rem;
background: transparent;
border-bottom: 2px solid #b4b4b4;
outline: none;
font-size: 1rem;
transition: border-bottom 0.1s ease-in-out;
&:focus {
border-bottom: 2px solid #ffb049;
}
`;

export default EmailEditor;
2 changes: 2 additions & 0 deletions packages/client/src/constants/properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export const API_URL = {
USER: {
GET_ME: `/users/me`,
GET_USER_BY_USERNAME: (username: string) => `/users/${username}`,
UPDATE_EMAIL: `/users/email`,
UPDATE_EMAIL_NOTIFICATION: `/users/email-notification`,
},
POST: {
GET_POSTS: (cursor?: string) =>
Expand Down
2 changes: 2 additions & 0 deletions packages/client/src/hooks/queries/user/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export { default as useGetMe } from './useGetMe';
export { default as useGetUserByUsername } from './useGetUserByUsername';
export { default as useUpdateEmail } from './useUpdateEmail';
export { default as useUpdateEmailNotification } from './useUpdateEmailNotification';
11 changes: 11 additions & 0 deletions packages/client/src/hooks/queries/user/useUpdateEmail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { UserAPI } from '~/lib/api';
import { useMutation } from '@tanstack/react-query';
import type { UseMutationOptionsOf } from '~/hooks/queries/types';

const useUpdateEmail = (
options: UseMutationOptionsOf<typeof UserAPI.updateEmail> = {},
) => {
return useMutation(UserAPI.updateEmail, options);
};

export default useUpdateEmail;
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { UserAPI } from '~/lib/api';
import { useMutation } from '@tanstack/react-query';
import type { UseMutationOptionsOf } from '~/hooks/queries/types';

const useUpdateEmailNotification = (
options: UseMutationOptionsOf<typeof UserAPI.updateEmailNotification> = {},
) => {
return useMutation(UserAPI.updateEmailNotification, options);
};

export default useUpdateEmailNotification;
21 changes: 20 additions & 1 deletion packages/client/src/lib/api/user.api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { API_URL } from '~/constants';
import { User } from '~/lib/types';
import {
UpdateEmailParams,
UpdateEmailNotificationParams,
User,
} from '~/lib/types';
import apiClient from './apiClient';

export const UserAPI = {
Expand All @@ -14,4 +18,19 @@ export const UserAPI = {
);
return data;
},

updateEmail: async (params: UpdateEmailParams): Promise<string | null> => {
const { data } = await apiClient.patch(API_URL.USER.UPDATE_EMAIL, params);
return data;
},

updateEmailNotification: async (
params: UpdateEmailNotificationParams,
): Promise<boolean> => {
const { data } = await apiClient.patch(
API_URL.USER.UPDATE_EMAIL_NOTIFICATION,
params,
);
return data;
},
};
Loading

0 comments on commit 911613d

Please sign in to comment.