Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add edit vote feature #510

Merged
merged 1 commit into from
Nov 4, 2024
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
7 changes: 6 additions & 1 deletion packages/api/src/listener/agenda.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as schema from "@biseo/interface/agenda";
import { retrieveAll, vote } from "@biseo/api/service/agenda";
import { retrieveAll, editVote, vote } from "@biseo/api/service/agenda";

import { Router } from "@biseo/api/lib/listener";

Expand All @@ -9,6 +9,11 @@ router.on("agenda.retrieveAll", schema.RetrieveAll, async (_, { user }) =>
retrieveAll(user),
);

router.on("agenda.edit", schema.EditVote, async (req, { io, user }) => {
await editVote(req, io, user);
return {};
});

router.on("agenda.vote", schema.Vote, async (req, { io, user }) => {
await vote(req, io, user);
return {};
Expand Down
48 changes: 48 additions & 0 deletions packages/api/src/service/agenda.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,54 @@ export const retrieveAll = async (
return res;
};

export const editVote = async (
{ choiceId, agendaId }: schema.Vote,
io: BiseoServer,
user: User,
) => {
const existingVote = await prisma.userChoice.findFirst({
where: {
userId: user.id,
choice: {
agendaId,
},
},
});

if (!existingVote) throw new BiseoError("No previous vote found");

await prisma.userChoice.delete({
where: {
userId_choiceId: {
userId: user.id,
choiceId: existingVote.choiceId,
},
},
});

await prisma.userChoice.create({
data: {
userId: user.id,
choiceId,
},
});

io.to(`user/${user.username}`).emit("agenda.voted", {
id: agendaId,
user: { voted: choiceId },
voters: {
voted: await prisma.userChoice.count({
where: {
choice: { agendaId },
},
}),
total: await prisma.userAgendaVotable.count({
where: { agendaId },
}),
},
});
};

export const vote = async (
{ choiceId, agendaId }: schema.Vote,
io: BiseoServer,
Expand Down
12 changes: 12 additions & 0 deletions packages/interface/src/agenda/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ export type RetrieveAll = z.infer<typeof RetrieveAll>;
export const RetrieveAllCb = z.array(Agenda);
export type RetrieveAllCb = z.infer<typeof RetrieveAllCb>;

/**
* Edit Vote
* description
*/
export const EditVote = z.object({
choiceId: z.number(),
agendaId: z.number(),
});
export type EditVote = z.infer<typeof EditVote>;
export const EditVoteCb = z.object({});
export type EditVoteCb = z.infer<typeof EditVoteCb>;

/**
* Vote
* description
Expand Down
2 changes: 2 additions & 0 deletions packages/interface/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type ClientToServerEvents = Events<{
agenda: {
retrieveAll: Ev<agenda.RetrieveAll, agenda.RetrieveAllCb>;
vote: Ev<agenda.Vote, agenda.VoteCb>;
edit: Ev<agenda.EditVote, agenda.EditVoteCb>;
template: {
create: Ev<agendaTemplate.Create, agendaTemplate.CreateCb>;
retrieveAll: Ev<agendaTemplate.RetrieveAll, agendaTemplate.RetrieveAllCb>;
Expand Down Expand Up @@ -59,6 +60,7 @@ export type ServerToClientEvents = Events<{
updated: Ev<agenda.Updated>;
started: Ev<agenda.Started>;
voted: Ev<agenda.Voted>;
voteEdited: Ev<agenda.Voted>;
terminated: Ev<agenda.Terminated>;
deleted: Ev<agenda.Deleted>;
reminded: Ev<agenda.Reminded>;
Expand Down
11 changes: 10 additions & 1 deletion packages/web/src/components/atoms/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,17 @@ export const Button = styled.button<{
w?: Size;
h?: Size;
color?: Color;
textColor?: Color;
padHorizontal?: number;
}>(
({ w = "fill", h = 28, color = "blue200", padHorizontal = 0, theme }) => css`
({
w = "fill",
h = 28,
color = "blue200",
textColor = "black",
padHorizontal = 0,
theme,
}) => css`
display: flex;
width: ${calcSize(w)};
height: ${calcSize(h)};
Expand All @@ -26,6 +34,7 @@ export const Button = styled.button<{
align-items: center;
line-height: 28px;
background-color: ${theme.colors[color]};
color: ${theme.colors[textColor]};

&:hover {
cursor: pointer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ const agendaTags = {

export const OngoingAgendaCard: React.FC<OngoingAgendaProps> = ({ agenda }) => {
const [chosenChoiceId, setChosenChoiceId] = useState(0);
const { voteAgenda } = useAgenda(state => ({
const [isEditing, setIsEditing] = useState(false);
const { voteAgenda, editVote } = useAgenda(state => ({
voteAgenda: state.voteAgenda,
editVote: state.editVote,
}));

const vote = useCallback(() => {
Expand All @@ -47,14 +49,20 @@ export const OngoingAgendaCard: React.FC<OngoingAgendaProps> = ({ agenda }) => {
[chosenChoiceId],
);

const submitEdit = useCallback(() => {
editVote(chosenChoiceId, agenda.id);
setIsEditing(false);
}, [chosenChoiceId, agenda.id]);

let choices: JSX.Element | JSX.Element[] = <NotVotableChoice />;
if (agenda.user.votable) {
if (agenda.user.voted) {
if (agenda.user.voted && !isEditing) {
choices = (
<CompletedChoice
choice={agenda.choices.find(
choice => choice.id === agenda.user.voted,
)}
onEdit={() => setIsEditing(true)}
/>
);
} else {
Expand Down Expand Up @@ -85,9 +93,20 @@ export const OngoingAgendaCard: React.FC<OngoingAgendaProps> = ({ agenda }) => {
<p css={[text.subtitle, text.gray500]}>{agenda.content}</p>
</div>
</div>
<div css={[column, gap(6)]}>
<p css={[text.body, text.blue600]}>{agenda.resolution}</p>
{choices}
<div css={[column, gap(10)]}>
<div css={[column, gap(6)]}>
<p css={[text.body, text.blue600]}>{agenda.resolution}</p>
{choices}
</div>
{isEditing && (
<div css={[row, justify.end, w("fill")]}>
<Button w={90} disabled={!chosen} onClick={submitEdit}>
<p css={[text.option1, chosen ? text.blue600 : text.blue300]}>
제출하기
</p>
</Button>
</div>
)}
</div>
{agenda.user.votable && !agenda.user.voted && (
<div css={[row, justify.end, w("fill")]}>
Expand Down
53 changes: 37 additions & 16 deletions packages/web/src/components/molecules/Choice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,19 @@ import styled from "@emotion/styled";
import { css } from "@emotion/react";

import type { Choice } from "@biseo/interface/agenda";
import { Text } from "@biseo/web/components/atoms";
import { Text, Button } from "@biseo/web/components/atoms";
import { SelectIcon } from "@biseo/web/assets";
import { type Color, theme } from "@biseo/web/theme";
import { center, h, w } from "@biseo/web/styles";
import {
gap,
column,
center,
row,
justify,
text,
h,
w,
} from "@biseo/web/styles";

const Container = styled.div<{
color: Color;
Expand Down Expand Up @@ -69,6 +78,7 @@ interface ChoiceBaseProps {

const ChoiceBase: React.FC<ChoiceBaseProps> = ({
variant,
// eslint-disable-next-line @typescript-eslint/no-shadow
text,
onClick = () => {},
onMouseEnter = () => {},
Expand Down Expand Up @@ -112,27 +122,38 @@ export const ChoiceComponent: React.FC<ChoiceProps> = ({

interface CompletedChoiceProps {
choice?: Choice;
onEdit: () => void;
}

export const CompletedChoice: React.FC<CompletedChoiceProps> = ({
choice = undefined,
onEdit,
}) => {
const [hover, setHover] = useState(false);

return hover ? (
<ChoiceBase
variant="hover"
text={choice?.name ?? ""}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
/>
) : (
<ChoiceBase
variant="chosen"
text="투표 완료"
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
/>
return (
<div css={[column, gap(10)]}>
{hover ? (
<ChoiceBase
variant="hover"
text={choice?.name ?? ""}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
/>
) : (
<ChoiceBase
variant="chosen"
text="투표 완료"
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
/>
)}
<div css={[row, justify.end, w("fill")]}>
<Button w={90} onClick={onEdit}>
<p css={[text.option1, text.blue600]}>수정하기</p>
</Button>
</div>
</div>
);
};

Expand Down
8 changes: 8 additions & 0 deletions packages/web/src/services/agenda.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
interface AgendaState {
agendas: Agenda[];
voteAgenda: (choiceId: number, agendaId: number) => void;
editVote: (choiceId: number, agendaId: number) => void;
retrieveAgendas: () => void;
}

Expand All @@ -17,6 +18,13 @@
// TODO: globally handle error using zustand middleware
}
},
editVote: async (choiceId, agendaId) => {
try {
await socket.emitAsync("agenda.edit", { choiceId, agendaId });
} catch (error) {
// TODO: globally handle error using zustand middleware
}
},
retrieveAgendas: async () => {
try {
const agendas = await socket.emitAsync("agenda.retrieveAll", {});
Expand Down Expand Up @@ -62,7 +70,7 @@

socket.on("agenda.reminded", reminded => {
// TODO: Fix the alert algorithm according to the design
alert(`Reminder Alert ${reminded.agendaId}: ${reminded.message}`);

Check warning on line 73 in packages/web/src/services/agenda.ts

View workflow job for this annotation

GitHub Actions / Lint and Format (18.x, 8.x)

Unexpected alert
});

export { useAgenda };
Loading
Loading