Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
57812a9
feat : add icon for appbar
yummjin Jun 25, 2025
d4e538a
feat : add icon at `AdminAppBar`
yummjin Jun 25, 2025
79e27ad
feat : add logout logic at `onLogoutClick` function
yummjin Jun 25, 2025
0c272bf
feat : implement logic fetches election data by status
yummjin Jun 25, 2025
7be72f0
feat : change `VoteStatus` type
yummjin Jun 25, 2025
1976edc
feat : show real data at admin dashboard screen
yummjin Jun 25, 2025
7eabc91
refactor : remove `Button` component at container due to make form co…
yummjin Jun 25, 2025
70dabe8
refactor : move `Notice` type to shared layer
yummjin Jun 25, 2025
24913f2
feat : add constant of `NoticeType`
yummjin Jun 25, 2025
a68448e
refactor : change type name and import
yummjin Jun 25, 2025
ec9c2f6
feat : implement logic submits notice creation
yummjin Jun 25, 2025
a08cb08
feat : pass parameters with `rest` for better flexibility
yummjin Jun 25, 2025
78ab710
feat : add `ElectionPost` type, `WholeCampus` type, util type `Replace`
yummjin Jun 25, 2025
3a702a5
feat : implement util function returns typed keys from object
yummjin Jun 25, 2025
e87aa50
refactor : reflect type change on `NoticeContentScreen`
yummjin Jun 25, 2025
85358c0
feat : complete implement of notice submitting functionality
yummjin Jun 25, 2025
93b91b4
refactor : change type imports
yummjin Jun 26, 2025
908c543
feat : add request and change past request
yummjin Jun 26, 2025
e82b0d8
feat : implement request logics to create vote
yummjin Jun 26, 2025
6ece9a6
feat : implement vote title form with real data
yummjin Jun 26, 2025
1ff9f8f
feat : implement `VoteTypeBottomSheet`
yummjin Jun 26, 2025
3c23995
feat : define type for all data of election
yummjin Jun 26, 2025
7696222
chore : change linter setting
yummjin Jun 26, 2025
2306551
chore : install `dotlottie-react` to implement animation
yummjin Jun 27, 2025
c5c0b39
feat : define `BaseResponse` type
yummjin Jun 27, 2025
1670789
feat : implement `DatePickerBottomSheet`
yummjin Jun 27, 2025
00f062e
feat : implement complete style to vote input and button components
yummjin Jun 27, 2025
76200f5
feat : implement `VoteCreateLoadingScreen`
yummjin Jun 27, 2025
9fe74a8
fix : fix endpoints
yummjin Jun 27, 2025
ee0452f
feat : separate form mode to custom hook
yummjin Jun 27, 2025
dc6e376
feat : define types about elections
yummjin Jun 27, 2025
060781d
feat : connect form data at `VoteForm`s
yummjin Jun 27, 2025
862685d
feat : add properties to vote create context
yummjin Jun 27, 2025
d7a87f0
feat : implement animation to `VoteCreateCompleteScreen`
yummjin Jun 27, 2025
025d1fd
feat: refactor submission functions to include election and candidate…
yummjin Jun 27, 2025
d5b48db
feat : add path constant for `VoteCreateLoadingScreen`
yummjin Jun 27, 2025
79d6c19
feat: refactor VoteCreateContainer to improve form handling and UI st…
yummjin Jun 27, 2025
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
5 changes: 5 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
as="image"
href="/src/assets/image/bg-vote-promise.png"
/>
<link
rel="preload"
as="image"
href="https://lottie.host/a0efbf08-977e-49af-926d-01b31d530dd8/7e8jbdwS7b.lottie"
/>
<link rel="preload" as="image" href="/src/assets/image/bg-vote.png" />
<link rel="preload" as="image" href="/src/assets/icon/icon-notice.png" />
<link rel="preload" as="image" href="/src/assets/icon/icon-result.png" />
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
"@lottiefiles/dotlottie-react": "^0.14.2",
"@stackflow/core": "^1.2.0",
"@stackflow/plugin-basic-ui": "^1.14.1",
"@stackflow/plugin-renderer-basic": "^1.1.13",
Expand Down
18 changes: 18 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/app/stackflow/Stack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { NoticeCreateScreen } from '@/screen/notice-create/ui';
import { NoticeScreen } from '@/screen/notice/ui';
import { VoteCompleteScreen } from '@/screen/vote-complete/ui';
import { VoteCreateCompleteScreen } from '@/screen/vote-create-complete/ui';
import { VoteCreateLoadingScreen } from '@/screen/vote-create-loading/ui';
import { VoteCreateScreen } from '@/screen/vote-create/ui';
import { VoteEditScreen } from '@/screen/vote-edit/ui';
import { VotePromiseScreen } from '@/screen/vote-promise/ui';
Expand All @@ -37,6 +38,7 @@ export const { Stack, useFlow } = stackflow({
VoteEditScreen,
VoteScreen,
VoteCreateScreen,
VoteCreateLoadingScreen,
VoteCreateCompleteScreen,
VoteCompleteScreen,
VotePromiseScreen,
Expand Down
3 changes: 3 additions & 0 deletions src/assets/icon/icon-user-square.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions src/assets/icon/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import NotificationIcon from './icon-notification.png';
import ResultIcon from './icon-result.png';
import TextBoxIcon from './icon-textbox.svg';
import UserIcon from './icon-user.svg';
import UserSquareIcon from './icon-user-square.svg';
import VoteCompleteIcon from './icon-vote-complete.png';
import VoteIcon from './icon-vote.svg';
import WriteIcon from './icon-write.svg';
Expand All @@ -38,6 +39,7 @@ export {
ResultIcon,
TextBoxIcon,
UserIcon,
UserSquareIcon,
VerifiedCheckIcon,
VoteCompleteIcon,
VoteIcon,
Expand Down
19 changes: 19 additions & 0 deletions src/features/admin-dashboard/api/election.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { REQUEST, userGet } from '@/shared/api';
import type { Election, VoteStatus } from '@/shared/types';
import { useQuery } from '@tanstack/react-query';

const fetchElectionByStatus = async (status: VoteStatus) => {
const response = await userGet<Election[], { status: VoteStatus }>({
request: REQUEST.ELECTION_STATUS,
params: { status: status },
});
return response.data;
};

export const useFetchElectionByStatus = (status: VoteStatus) => {
return useQuery({
queryKey: ['election-status', `${status}`],
queryFn: () => fetchElectionByStatus(status),
staleTime: 60 * 5,
});
};
1 change: 1 addition & 0 deletions src/features/admin-dashboard/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './election';
63 changes: 45 additions & 18 deletions src/features/admin-dashboard/ui/CardList.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,52 @@
import type { VoteStatus } from '@/shared/types';
// import { Card } from '@/shared/ui';
import { VOTE_MOCK } from '@/features/admin-dashboard/mock';
import type { Election, VoteStatus } from '@/shared/types';
import { Card } from '@/shared/ui';
import { getDate } from '@/shared/utils';

export default function CardList({ status }: { status: VoteStatus }) {
const data = VOTE_MOCK.filter(
({ status: voteStatus }) => voteStatus === status,
import { useFetchElectionByStatus } from '@/features/admin-dashboard/api';

export default function CardList({ status }: { status: string }) {
const VOTE_STATUS: Record<string, VoteStatus> = {
진행중: 'ongoing',
예정: 'upcoming',
종료: 'ended',
};
const { data, isError, isFetching } = useFetchElectionByStatus(
VOTE_STATUS[status],
);
console.log(data);


const renderElection = (data: Election[]) => {
if (isError)
return (
<div className="px-normal mt-8 w-full text-center">
투표를 가져오던 중 오류가 발생했어요!
</div>
);
if (!isFetching && data.length === 0)
return (
<div className="px-normal mt-8 w-full text-center">투표가 없어요</div>
);
if (data && data.length > 0)
return (
<>
{data.map(({ id, campus, title, startAt, endAt }, index) => (
<Card
className={index === 0 ? 'mt-8' : ''}
id={id}
key={id}
campus={campus}
status={status}
title={title}
date={`${getDate(startAt, 'YYYY.MM.DD')} - ${getDate(endAt, 'YYYY.MM.DD')}`}
/>
))}
</>
);
return <></>;
};

return (
<div className="scrollbar-hide px-normal flex size-full flex-1 flex-col gap-y-[26px] overflow-scroll">
{/* {data.map(({ id, campus, status, title, date }, index) => (
<Card
className={index === 0 ? 'mt-8' : ''}
key={id}
campus={campus}
status={status}
title={title}
date={date}
/>
))} */}
{renderElection(data || [])}
</div>
);
}
26 changes: 26 additions & 0 deletions src/features/notice-create/api/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useFlow } from '@/app/stackflow';
import { useMutation } from '@tanstack/react-query';

import { REQUEST, userPost } from '@/shared/api';
import type { Replace, WholeCampus, Notice } from '@/shared/types';

const submitNotice = async (data: Replace<Notice, 'campus', WholeCampus>) => {
const response = await userPost({
request: REQUEST.NOTICE.slice(0, -1),
data: data,
});
return response;
};

export const useSubmitNotice = () => {
const { pop } = useFlow();

return useMutation({
mutationFn: (data: Replace<Notice, 'campus', WholeCampus>) =>
submitNotice(data),
onSuccess: () => {
alert('공지 등록 성공!');
pop({ animate: false });
},
});
};
1 change: 1 addition & 0 deletions src/features/notice-create/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './create';
127 changes: 100 additions & 27 deletions src/features/notice-create/ui/NoticeForm.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,108 @@
import { useForm } from 'react-hook-form';
import { useEffect, useState } from 'react';

import type {
Campus,
Replace,
WholeCampus,
Notice,
NoticeType,
} from '@/shared/types';
import { CAMPUS, NOTICE_TYPE } from '@/shared/constants';
import { keys } from '@/shared/utils';
import { Button } from '@/shared/ui';

import { useSubmitNotice } from '@/features/notice-create/api';

import NoticeFormButton from './NoticeFormButton';
import NoticeFormInput from './NoticeFormInput';
import NoticeFormItem from './NoticeFormItem';

export default function NoticeForm() {
const { mutate } = useSubmitNotice();
const [noticeType, setNoticeType] = useState<NoticeType | undefined>();
const [campus, setCampus] = useState<Campus[]>([]);
const { register, handleSubmit, watch, setValue } = useForm<
Replace<Notice, 'campus', WholeCampus>
>({
defaultValues: {
startAt: '2025-06-27',
endAt: '2025-06-28',
},
});
const isFormValid =
watch('title') &&
watch('campus') &&
watch('content') &&
watch('noticeType');

useEffect(() => {
const value = campus.length > 1 ? ('ALL' as const) : campus[0];
setValue('campus', value);
}, [campus, setValue]);

return (
<div className="p-normal scrollbar-hide size-full space-y-6 overflow-scroll pb-38">
<NoticeFormItem label="공지 유형이 무엇인가요?" required>
<div className="items-center space-x-[10px]">
<NoticeFormButton label="알림" />
<NoticeFormButton label="선거 공지" />
</div>
</NoticeFormItem>
<NoticeFormItem label="어떤 캠퍼스 공지인가요?">
<div className="items-center space-x-[10px]">
<NoticeFormButton label="수원캠" />
<NoticeFormButton label="서울캠" />
</div>
</NoticeFormItem>
<NoticeFormItem label="기간">
<div className="flex w-full items-center gap-x-[10px]">
<div className="border-sl h-[46px] flex-1 rounded-md border-[1px] px-5 py-[11px] focus:outline-none" />
~
<div className="border-sl h-[46px] flex-1 rounded-md border-[1px] px-5 py-[11px] focus:outline-none" />
</div>
</NoticeFormItem>
<NoticeFormItem label="제목">
<NoticeFormInput />
</NoticeFormItem>
<NoticeFormItem label="내용">
<textarea className="border-sl h-42 w-full resize-none rounded-md border-[1px] px-5 py-[18px] focus:outline-none" />
</NoticeFormItem>
</div>
<form onSubmit={handleSubmit(data => mutate(data))}>
<div className="p-normal scrollbar-hide size-full space-y-6 overflow-scroll pb-38">
<NoticeFormItem label="공지 유형이 무엇인가요?" required>
<div className="items-center space-x-[10px]">
{keys(NOTICE_TYPE).map(noti => (
<NoticeFormButton
key={noti}
label={NOTICE_TYPE[noti]}
selected={noti === noticeType}
onClick={() => {
setNoticeType(noti);
setValue('noticeType', noti);
}}
/>
))}
</div>
</NoticeFormItem>
<NoticeFormItem label="어떤 캠퍼스 공지인가요?">
<div className="items-center space-x-[10px]">
{keys(CAMPUS).map(cam => (
<NoticeFormButton
key={cam}
label={CAMPUS[cam]}
selected={campus.includes(cam)}
onClick={() => {
setCampus(prev => {
if (!prev?.includes(cam)) return [cam, ...prev];
else return prev.filter(c => c !== cam);
});
}}
/>
))}
</div>
</NoticeFormItem>
<NoticeFormItem label="기간">
<div className="flex w-full items-center gap-x-[10px]">
<div className="border-sl h-[46px] flex-1 rounded-md border-[1px] px-5 py-[11px] focus:outline-none" />
~
<div className="border-sl h-[46px] flex-1 rounded-md border-[1px] px-5 py-[11px] focus:outline-none" />
</div>
</NoticeFormItem>
<NoticeFormItem label="제목">
<NoticeFormInput {...register('title')} />
</NoticeFormItem>
<NoticeFormItem label="내용">
<textarea
className="border-sl h-42 w-full resize-none rounded-md border-[1px] px-5 py-[18px] focus:outline-none"
{...register('content')}
/>
</NoticeFormItem>
</div>
<div className="shadow-voteCreateDock px-normal fixed bottom-0 flex w-full gap-x-[9px] bg-white pt-4 pb-15">
<Button
intent={isFormValid ? 'gradient' : 'disabled'}
disabled={!isFormValid}
className="flex-1 text-lg"
type="submit"
>
작성 완료
</Button>
</div>
</form>
);
}
19 changes: 15 additions & 4 deletions src/features/notice-create/ui/NoticeFormButton.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
import { cn } from '@/shared/utils';
import { useState } from 'react';
import type { HTMLAttributes } from 'react';

export default function NoticeFormButton({ label }: { label: string }) {
const [selected, setSelected] = useState(false);
interface NoticeFormButtonProps extends HTMLAttributes<HTMLButtonElement> {
label: string;
selected: boolean;
}

export default function NoticeFormButton({
label,
selected,
onClick,
...rest
}: NoticeFormButtonProps) {
return (
<button
onClick={() => setSelected(!selected)}
type="button"
onClick={onClick}
className={cn(
selected ? 'text-m border-m' : 'border-sl text-s',
'cursor-pointer rounded-md border-[1px] px-[14px] py-1 text-sm focus:outline-none',
)}
{...rest}
>
{label}
</button>
Expand Down
11 changes: 9 additions & 2 deletions src/features/notice-create/ui/NoticeFormInput.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
export default function NoticeFormInput() {
import type { HTMLAttributes } from 'react';

type NoticeFormInputProps = HTMLAttributes<HTMLInputElement>;

export default function NoticeFormInput({ ...rest }: NoticeFormInputProps) {
return (
<input className="border-sl w-full rounded-md border-[1px] px-5 py-[11px] focus:outline-none" />
<input
className="border-sl w-full rounded-md border-[1px] px-5 py-[11px] focus:outline-none"
{...rest}
/>
);
}
Loading