- 시작/종료 시간 설정 화면 및 로직 구현 - 일정 상세 사항 화면 및 로직 구현 - 참여자 추가 화면 구현 중
This commit is contained in:
@@ -19,6 +19,7 @@
|
|||||||
"hooks": "@/hooks"
|
"hooks": "@/hooks"
|
||||||
},
|
},
|
||||||
"registries": {
|
"registries": {
|
||||||
"@reui": "https://reui.io/r/{name}.json"
|
"@reui": "https://reui.io/r/{name}.json",
|
||||||
|
"@diceui": "https://diceui.com/r/{name}.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
67
package-lock.json
generated
67
package-lock.json
generated
@@ -8,6 +8,7 @@
|
|||||||
"name": "scheduler",
|
"name": "scheduler",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@diceui/mention": "^0.8.0",
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@radix-ui/react-accordion": "^1.2.12",
|
"@radix-ui/react-accordion": "^1.2.12",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
@@ -376,6 +377,50 @@
|
|||||||
"integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==",
|
"integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@diceui/mention": {
|
||||||
|
"version": "0.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@diceui/mention/-/mention-0.8.0.tgz",
|
||||||
|
"integrity": "sha512-BHTYLG4qJA+9TJ/n5otlIHoJuLz5v13xAayYsRL7lnCnp4ik37uYrLpA+25y+uQmJ5i1W3kv2MfD2ZasvDzDDw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@diceui/shared": "0.12.0",
|
||||||
|
"@floating-ui/react": "^0.27.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@diceui/shared": {
|
||||||
|
"version": "0.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@diceui/shared/-/shared-0.12.0.tgz",
|
||||||
|
"integrity": "sha512-aJ+gxHTleFD8b9DgXOb7G0sHymW8nMK4C3KXDMca4+PPl1NsfRG/Lnb2dN8vq44XOb/W7Wjn3gmgakXAOzgrJA==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@floating-ui/react": "^0.27.4",
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.25.11",
|
"version": "0.25.11",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz",
|
||||||
@@ -968,6 +1013,22 @@
|
|||||||
"@floating-ui/utils": "^0.2.10"
|
"@floating-ui/utils": "^0.2.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@floating-ui/react": {
|
||||||
|
"version": "0.27.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.16.tgz",
|
||||||
|
"integrity": "sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/react-dom": "^2.1.6",
|
||||||
|
"@floating-ui/utils": "^0.2.10",
|
||||||
|
"tabbable": "^6.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=17.0.0",
|
||||||
|
"react-dom": ">=17.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@floating-ui/react-dom": {
|
"node_modules/@floating-ui/react-dom": {
|
||||||
"version": "2.1.6",
|
"version": "2.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz",
|
||||||
@@ -6375,6 +6436,12 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tabbable": {
|
||||||
|
"version": "6.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.3.0.tgz",
|
||||||
|
"integrity": "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tailwind-merge": {
|
"node_modules/tailwind-merge": {
|
||||||
"version": "3.4.0",
|
"version": "3.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@diceui/mention": "^0.8.0",
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@radix-ui/react-accordion": "^1.2.12",
|
"@radix-ui/react-accordion": "^1.2.12",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
|
|||||||
91
src/components/ui/mention.tsx
Normal file
91
src/components/ui/mention.tsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import * as MentionPrimitive from "@diceui/mention";
|
||||||
|
import type * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
function Mention({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof MentionPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<MentionPrimitive.Root
|
||||||
|
data-slot="mention"
|
||||||
|
className={cn(
|
||||||
|
"**:data-tag:rounded **:data-tag:bg-blue-200 **:data-tag:py-px **:data-tag:text-blue-950 dark:**:data-tag:bg-blue-800 dark:**:data-tag:text-blue-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MentionLabel({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof MentionPrimitive.Label>) {
|
||||||
|
return (
|
||||||
|
<MentionPrimitive.Label
|
||||||
|
data-slot="mention-label"
|
||||||
|
className={cn("px-0.5 py-1.5 font-semibold text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MentionInput({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof MentionPrimitive.Input>) {
|
||||||
|
return (
|
||||||
|
<MentionPrimitive.Input
|
||||||
|
data-slot="mention-input"
|
||||||
|
className={cn(
|
||||||
|
"flex w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs placeholder:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MentionContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof MentionPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<MentionPrimitive.Portal>
|
||||||
|
<MentionPrimitive.Content
|
||||||
|
data-slot="mention-content"
|
||||||
|
className={cn(
|
||||||
|
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</MentionPrimitive.Content>
|
||||||
|
</MentionPrimitive.Portal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MentionItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof MentionPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<MentionPrimitive.Item
|
||||||
|
data-slot="mention-item"
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</MentionPrimitive.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Mention, MentionContent, MentionInput, MentionItem, MentionLabel };
|
||||||
@@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
|||||||
<textarea
|
<textarea
|
||||||
data-slot="textarea"
|
data-slot="textarea"
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import * as z from 'zod';
|
|||||||
export const CreateScheduleSchema = z.object({
|
export const CreateScheduleSchema = z.object({
|
||||||
name: z
|
name: z
|
||||||
.string()
|
.string()
|
||||||
|
.nonempty()
|
||||||
, startDate: z
|
, startDate: z
|
||||||
.date()
|
.date()
|
||||||
, endDate: z
|
, endDate: z
|
||||||
@@ -21,4 +22,6 @@ export const CreateScheduleSchema = z.object({
|
|||||||
.string()
|
.string()
|
||||||
, dayList: z
|
, dayList: z
|
||||||
.string()
|
.string()
|
||||||
|
, participantList: z
|
||||||
|
.array(z.string())
|
||||||
});
|
});
|
||||||
@@ -6,7 +6,7 @@ export const LoginSchema = z.object({
|
|||||||
.string()
|
.string()
|
||||||
.refine((val) => {
|
.refine((val) => {
|
||||||
if (val.includes('@')) {
|
if (val.includes('@')) {
|
||||||
return Validator.isEmail(val);;
|
return Validator.isEmail(val);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}, {
|
}, {
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import * as z from 'zod';
|
|||||||
export const SignUpSchema = z.object({
|
export const SignUpSchema = z.object({
|
||||||
accountId: z
|
accountId: z
|
||||||
.string()
|
.string()
|
||||||
.min(5, "아이디는 5 자리 이상이어야 합니다.")
|
.min(5, "아이디는 6-14자여야 합니다.")
|
||||||
|
.max(14, "아이디는 6-14자여야합니다.")
|
||||||
.refine((val) => {
|
.refine((val) => {
|
||||||
return /^[a-zA-z-_.]*$/.test(val);
|
return /^[a-zA-z-_.]*$/.test(val);
|
||||||
}, {
|
}, {
|
||||||
@@ -14,12 +15,13 @@ export const SignUpSchema = z.object({
|
|||||||
.min(5, "이메일을 입력해주십시오.")
|
.min(5, "이메일을 입력해주십시오.")
|
||||||
, password: z
|
, password: z
|
||||||
.string()
|
.string()
|
||||||
.min(8, "비밀번호는 8-12 자리여야 합니다.")
|
.min(8, "비밀번호는 8-12자여야 합니다.")
|
||||||
.max(12, "비밀번호는 8-12 자리여야 합니다.")
|
.max(12, "비밀번호는 8-12자여야 합니다.")
|
||||||
.regex(/^(?=.*[0-9])(?=.*[!@#$%^])[a-zA-Z0-9!@#$%^]+$/, "영소문자로 시작하고 숫자와 특수문자(!@#$%^)를 포함해야 합니다.")
|
.regex(/^(?=.*[0-9])(?=.*[!@#$%^])[a-zA-Z0-9!@#$%^]+$/, "영소문자로 시작하고 숫자와 특수문자(!@#$%^)를 포함해야 합니다.")
|
||||||
, name: z
|
, name: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, "이름을 입력해주시십시오.")
|
.min(1, "이름을 입력해주시십시오.")
|
||||||
|
.max(14, "너무 긴 이름은 사용하실 수 없습니다.")
|
||||||
, nickname: z
|
, nickname: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, "닉네임을 입력해주십시오.")
|
.min(1, "닉네임을 입력해주십시오.")
|
||||||
|
|||||||
21
src/hooks/use-time.ts
Normal file
21
src/hooks/use-time.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
export function useTime() {
|
||||||
|
const getNowString = () => {
|
||||||
|
const now = new Date();
|
||||||
|
const hour = now.getHours();
|
||||||
|
const minute = now.getMinutes();
|
||||||
|
const ampm = hour < 12 ? '오전' : '오후';
|
||||||
|
|
||||||
|
return `${ampm} ${hour > 12 ? hour - 12 : hour}시 ${minute.toString().padStart(2, '0')}분`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeStringToISOString = (time: string) => {
|
||||||
|
const timeArray = time.split(' ');
|
||||||
|
const hour = timeArray[0] === '오전' ? Number(timeArray[1]) : Number(timeArray[1]) + 12;
|
||||||
|
return `${hour.toString().padStart(2, '0')}:${timeArray[2]}:00`
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
getNowString,
|
||||||
|
timeStringToISOString
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,15 +56,15 @@ export const DatePickPopover = ({ ...props } : DaetPickPopoverProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="w-full h-full flex flex-row justify-around items-center"
|
className="w-full h-full flex flex-row justify-between gap-3 items-center"
|
||||||
>
|
>
|
||||||
<Popover
|
<Popover
|
||||||
open={startOpen}
|
open={startOpen}
|
||||||
onOpenChange={setStartOpen}
|
onOpenChange={setStartOpen}
|
||||||
>
|
>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
className="border border-indigo-100 bg-white hover:bg-indigo-100 text-black"
|
className="flex-9 h-full border border-indigo-100 bg-white hover:bg-indigo-100 text-black"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
@@ -91,14 +91,14 @@ export const DatePickPopover = ({ ...props } : DaetPickPopoverProps) => {
|
|||||||
/>
|
/>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
<span> - </span>
|
<div className="flex-2 h-px bg-indigo-300" />
|
||||||
<Popover
|
<Popover
|
||||||
open={endOpen}
|
open={endOpen}
|
||||||
onOpenChange={setEndOpen}
|
onOpenChange={setEndOpen}
|
||||||
>
|
>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
className="border border-indigo-100 bg-white hover:bg-indigo-100 text-black"
|
className="flex-9 h-full border border-indigo-100 bg-white hover:bg-indigo-100 text-black"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
|
|||||||
101
src/ui/component/popover/schedule/ParticipantPopover.tsx
Normal file
101
src/ui/component/popover/schedule/ParticipantPopover.tsx
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import * as Mention from '@diceui/mention';
|
||||||
|
import { AtSign, PlusIcon, SearchIcon } from "lucide-react";
|
||||||
|
import { useRef, useState } from "react";
|
||||||
|
|
||||||
|
const dummyUser = [
|
||||||
|
{
|
||||||
|
accountId: "dummy1",
|
||||||
|
name: "더미1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accountId: "test2",
|
||||||
|
name: "테스트2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accountId: "dummy3",
|
||||||
|
name: "테스트3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accountId: "test4",
|
||||||
|
name: "더미4"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export const ParticipantPopover = () => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [filterString, setFilterString] = useState('');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full h-10 flex flex-row justify-start items-center">
|
||||||
|
<Popover
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
className={cn(
|
||||||
|
!open
|
||||||
|
? "bg-white text-indigo-300 hover:bg-indigo-300 hover:text-white"
|
||||||
|
: "bg-indigo-300 text-white hover:bg-indigo-300!"
|
||||||
|
|
||||||
|
)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<PlusIcon
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
참여자 추가
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
className="py-0 px-0 w-60 h-80 flex flex-col justify-start items-start"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="flex flex-row items-center px-3 h-10 w-full border-b border-b-indigo-100"
|
||||||
|
>
|
||||||
|
<SearchIcon size={16} className="stroke-indigo-300" />
|
||||||
|
<Input
|
||||||
|
placeholder="검색"
|
||||||
|
value={filterString}
|
||||||
|
className="shadow-none placeholder-indigo-300! focus-visible:placeholder-indigo-400! flex-1 focus-visible:ring-0 focus-visible:border-0 border-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ScrollArea
|
||||||
|
className="w-full h-60 flex flex-col"
|
||||||
|
>
|
||||||
|
|
||||||
|
</ScrollArea>
|
||||||
|
<div
|
||||||
|
className="border-t border-t-indigo-300 w-full h-10 flex flex-row cursor-default"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex-5 h-full flex flex-row justify-center items-center transition-all duration-150",
|
||||||
|
"bg-white text-indigo-300",
|
||||||
|
"hover:bg-indigo-300 hover:text-white"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
확인
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex-5 h-full flex flex-row justify-center items-center transition-all duration-150",
|
||||||
|
"bg-white text-red-400",
|
||||||
|
"hover:bg-red-400 hover:text-white"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
취소
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ import { ScrollArea } from '@/components/ui/scroll-area';
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { usePalette } from '@/hooks/use-palette';
|
import { usePalette } from '@/hooks/use-palette';
|
||||||
import { ColorPalette, type ColorPaletteType } from '@/const/ColorPalette';
|
import { type ColorPaletteType } from '@/const/ColorPalette';
|
||||||
import { ColorPickPopover } from './ColorPickPopover';
|
import { ColorPickPopover } from './ColorPickPopover';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
@@ -20,6 +20,8 @@ import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
|||||||
import { DatePickPopover } from './DatePickPopover';
|
import { DatePickPopover } from './DatePickPopover';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { TimePickPopover } from './TimePickPopover';
|
import { TimePickPopover } from './TimePickPopover';
|
||||||
|
import { useTime } from '@/hooks/use-time';
|
||||||
|
import { ScheduleCreateContent } from './content/ScheduleCreateContent';
|
||||||
|
|
||||||
interface ScheduleSheetProps {
|
interface ScheduleSheetProps {
|
||||||
date: Date | undefined;
|
date: Date | undefined;
|
||||||
@@ -29,85 +31,8 @@ interface ScheduleSheetProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const SchedulePopover = ({ date, open, popoverSide, popoverAlign }: ScheduleSheetProps) => {
|
export const SchedulePopover = ({ date, open, popoverSide, popoverAlign }: ScheduleSheetProps) => {
|
||||||
const {
|
|
||||||
getPaletteByKey
|
|
||||||
} = usePalette();
|
|
||||||
const [mode, setMode] = useState<'list' | 'create' | 'detail' | 'update'>('list');
|
const [mode, setMode] = useState<'list' | 'create' | 'detail' | 'update'>('list');
|
||||||
const [colorPopoverOpen, setColorPopoverOpen] = useState(false);
|
|
||||||
const dayLabelList = useRecord(ScheduleDay).keys.map((key) => {
|
|
||||||
return {
|
|
||||||
day: Number(key),
|
|
||||||
label: ScheduleDay[Number(key)]
|
|
||||||
} as { day: number, label: string };
|
|
||||||
})
|
|
||||||
const createScheduleForm = useForm<z.infer<typeof CreateScheduleSchema>>({
|
|
||||||
resolver: zodResolver(CreateScheduleSchema),
|
|
||||||
defaultValues: {
|
|
||||||
name: "",
|
|
||||||
startDate: date || new Date(),
|
|
||||||
endDate: date || new Date(),
|
|
||||||
content: "",
|
|
||||||
startTime: "",
|
|
||||||
endTime: "",
|
|
||||||
type: "once",
|
|
||||||
status: "yet",
|
|
||||||
style: getPaletteByKey('Black').style,
|
|
||||||
dayList: ""
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log(date);
|
|
||||||
if (open && date) {
|
|
||||||
createScheduleForm.setValue('startDate', date);
|
|
||||||
createScheduleForm.setValue('endDate', date);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
setMode('list');
|
|
||||||
createScheduleForm.clearErrors();
|
|
||||||
createScheduleForm.reset();
|
|
||||||
}, 150);
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
startDate,
|
|
||||||
endDate,
|
|
||||||
content,
|
|
||||||
startTime,
|
|
||||||
endTime,
|
|
||||||
type,
|
|
||||||
status,
|
|
||||||
style,
|
|
||||||
dayList
|
|
||||||
} = createScheduleForm.watch();
|
|
||||||
|
|
||||||
const selectColor = (color: ColorPaletteType) => {
|
|
||||||
createScheduleForm.setValue('style', color.style);
|
|
||||||
setColorPopoverOpen(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectType = (type: ScheduleType) => {
|
|
||||||
createScheduleForm.setValue('type', type);
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectDayList = (newValues: string[]) => {
|
|
||||||
const sortedValues = newValues.sort();
|
|
||||||
const newDayList = sortedValues.join('');
|
|
||||||
createScheduleForm.setValue('dayList', newDayList);
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectDate = (type: 'startDate' | 'endDate', date: Date | undefined) => {
|
|
||||||
if (!date) return;
|
|
||||||
createScheduleForm.setValue(type, date);
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectTime = (type: 'startTime' | 'endTime', time: string) => {
|
|
||||||
createScheduleForm.setValue(type, time);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ListContent = () => {
|
const ListContent = () => {
|
||||||
return (
|
return (
|
||||||
@@ -117,143 +42,6 @@ export const SchedulePopover = ({ date, open, popoverSide, popoverAlign }: Sched
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const CreateContent = () => {
|
|
||||||
|
|
||||||
const OnceContent = () => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="w-full h-10"
|
|
||||||
>
|
|
||||||
<DatePickPopover
|
|
||||||
disabled={false}
|
|
||||||
mode={'range'}
|
|
||||||
popoverAlign={popoverAlign}
|
|
||||||
startDate={startDate}
|
|
||||||
endDate={endDate}
|
|
||||||
setStartDate={(date: Date | undefined) => selectDate('startDate', date)}
|
|
||||||
setEndDate={(date: Date | undefined) => selectDate('endDate', date)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const DailyContent = () => {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DefaultContent = () => {
|
|
||||||
return (
|
|
||||||
(
|
|
||||||
<ToggleGroup
|
|
||||||
type="multiple"
|
|
||||||
className="w-full h-10 flex flex-row justify-center items-center"
|
|
||||||
onValueChange={selectDayList}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
dayLabelList.map((day) => (
|
|
||||||
<ToggleGroupItem
|
|
||||||
value={`${day.day}`}
|
|
||||||
className="border rounded-none not-last:border-r-0"
|
|
||||||
>
|
|
||||||
{day.label}
|
|
||||||
</ToggleGroupItem>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</ToggleGroup>
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderContent = () => {
|
|
||||||
switch (type) {
|
|
||||||
case 'Once':
|
|
||||||
return <OnceContent />;
|
|
||||||
case 'Daily':
|
|
||||||
return <DailyContent />;
|
|
||||||
default:
|
|
||||||
return <DefaultContent />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full h-full flex flex-col justify-start items-start gap-4">
|
|
||||||
<div className="w-full flex flex-row justify-center items-center gap-5">
|
|
||||||
<Popover open={colorPopoverOpen} onOpenChange={setColorPopoverOpen}>
|
|
||||||
<div className="w-6 h-6 flex justify-center items-center">
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'rounded-full w-5 h-5 border-2 border-gray-300',
|
|
||||||
)}
|
|
||||||
style={{
|
|
||||||
backgroundColor: `${style}`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</PopoverTrigger>
|
|
||||||
</div>
|
|
||||||
<ColorPickPopover
|
|
||||||
setColor={selectColor}
|
|
||||||
/>
|
|
||||||
</Popover>
|
|
||||||
<Controller
|
|
||||||
name="name"
|
|
||||||
control={createScheduleForm.control}
|
|
||||||
render={({ field, fieldState }) => (
|
|
||||||
<Field data-invalid={fieldState.invalid}>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
id="form-create-schedule-name"
|
|
||||||
placeholder="제목"
|
|
||||||
className="font-bold border-t-0 border-r-0 border-l-0 p-0 border-b-2 rounded-none shadow-none border-indigo-300 focus-visible:ring-0 focus-visible:border-b-indigo-500"
|
|
||||||
style={{
|
|
||||||
fontSize: '20px'
|
|
||||||
}}
|
|
||||||
tabIndex={1}
|
|
||||||
arai-invalid={fieldState.invalid}
|
|
||||||
/>
|
|
||||||
<FieldError errors={[fieldState.error]} />
|
|
||||||
</Field>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<div className="hover:bg-gray-100 cursor-default w-full h-10 border flex justify-center items-center rounded-sm">
|
|
||||||
{ScheduleTypeLabel[type as keyof typeof ScheduleTypeLabel]}
|
|
||||||
</div>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<TypePickPopover
|
|
||||||
setType={selectType}
|
|
||||||
popoverSide={popoverSide}
|
|
||||||
/>
|
|
||||||
</Popover>
|
|
||||||
<div
|
|
||||||
className="w-full h-10"
|
|
||||||
>
|
|
||||||
{renderContent()}
|
|
||||||
</div>
|
|
||||||
<div className="w-full h-10">
|
|
||||||
<TimePickPopover
|
|
||||||
mode='range'
|
|
||||||
popoverAlign={popoverAlign}
|
|
||||||
disabled={false}
|
|
||||||
startTime=''
|
|
||||||
setStartTime={(time: string | undefined) => {}}
|
|
||||||
endTime=''
|
|
||||||
setEndTime={(time: string | undefined) => {}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="flex flex-row self-end justify-self-end items-center justify-center cursor-default"
|
|
||||||
onClick={() => setMode('list')}
|
|
||||||
>
|
|
||||||
<X />
|
|
||||||
<span>취소</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const DetailContent = () => {
|
const DetailContent = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -275,7 +63,13 @@ export const SchedulePopover = ({ date, open, popoverSide, popoverAlign }: Sched
|
|||||||
case 'list':
|
case 'list':
|
||||||
return <ListContent />
|
return <ListContent />
|
||||||
case 'create':
|
case 'create':
|
||||||
return <CreateContent />
|
return <ScheduleCreateContent
|
||||||
|
setMode={setMode}
|
||||||
|
date={date}
|
||||||
|
popoverAlign={popoverAlign}
|
||||||
|
popoverSide={popoverSide}
|
||||||
|
open={open}
|
||||||
|
/>
|
||||||
case 'detail':
|
case 'detail':
|
||||||
return <DetailContent />
|
return <DetailContent />
|
||||||
case 'update':
|
case 'update':
|
||||||
@@ -288,11 +82,10 @@ export const SchedulePopover = ({ date, open, popoverSide, popoverAlign }: Sched
|
|||||||
className="rounded-xl xl:w-[calc(100vw/4)] xl:max-w-[480px] min-w-[384px]"
|
className="rounded-xl xl:w-[calc(100vw/4)] xl:max-w-[480px] min-w-[384px]"
|
||||||
align={popoverAlign} side={popoverSide}
|
align={popoverAlign} side={popoverSide}
|
||||||
>
|
>
|
||||||
<div>{date && format(date, "yyyy년 MM월 dd일")}</div>
|
|
||||||
<ScrollArea
|
<ScrollArea
|
||||||
className={
|
className={
|
||||||
cn(
|
cn(
|
||||||
"min-h-[125px] h-[calc(100vh/2.5)] p-2.5 w-full flex flex-col",
|
"min-h-[125px] h-[calc(100vh/2.3)] w-full flex flex-col",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
interface BaseProps {
|
interface BaseProps {
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
@@ -33,20 +34,141 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// const { startTime, setStartTime, endTime, setEndTime } = props;
|
const [startOpen, setStartOpen] = useState(false);
|
||||||
|
const [endOpen, setEndOpen] = useState(false);
|
||||||
|
const { startTime, setStartTime, endTime, setEndTime } = props;
|
||||||
|
|
||||||
|
const onStartOpenChange = (open: boolean) => {
|
||||||
|
if (open) {
|
||||||
|
setStartOpen(true);
|
||||||
|
if (startTime) {
|
||||||
|
const startTimeArray = startTime.split(' ');
|
||||||
|
setTimeout(() => {
|
||||||
|
const startAmPmList = document.querySelectorAll('#startAmPm button');
|
||||||
|
const targetAmPm = Array.from(startAmPmList).find(button => {
|
||||||
|
return button.textContent.trim() === startTimeArray[0];
|
||||||
|
});
|
||||||
|
const startHourList = document.querySelectorAll('#startHour button');
|
||||||
|
const targetHour = Array.from(startHourList).find(button => {
|
||||||
|
return button.textContent.trim() === startTimeArray[1].slice(0, 2);
|
||||||
|
});
|
||||||
|
const startMinuteList = document.querySelectorAll('#startMinute button');
|
||||||
|
const targetMinute = Array.from(startMinuteList).find(button => {
|
||||||
|
return button.textContent.trim() === startTimeArray[2].slice(0, 2);
|
||||||
|
});
|
||||||
|
[targetAmPm, targetHour, targetMinute].forEach(element => {
|
||||||
|
(element as HTMLButtonElement).click();
|
||||||
|
});
|
||||||
|
}, 150);
|
||||||
|
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setStartOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const applyStartTime = () => {
|
||||||
|
const startOnStateElements = document.querySelectorAll('[id^="start"] [data-state="on"]');
|
||||||
|
if (startOnStateElements.length < 3) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
startAmPm,
|
||||||
|
startHour,
|
||||||
|
startMinute
|
||||||
|
} = {
|
||||||
|
startAmPm: document.querySelector('#startAmPm [data-state="on"]')?.textContent.trim(),
|
||||||
|
startHour: document.querySelector('#startHour [data-state="on"]')?.textContent.trim(),
|
||||||
|
startMinute: document.querySelector('#startMinute [data-state="on"]')?.textContent.trim()
|
||||||
|
};
|
||||||
|
const startTime = `${startAmPm} ${startHour}시 ${startMinute}분`;
|
||||||
|
setStartTime(startTime);
|
||||||
|
}
|
||||||
|
const cancelStartTime = () => {
|
||||||
|
setStartOpen(false);
|
||||||
|
setTimeout(() => {
|
||||||
|
const startOnElements = document.querySelectorAll('[id^="start"] [data-state="on"]');
|
||||||
|
startOnElements.forEach(element => {
|
||||||
|
(element as HTMLButtonElement).click();
|
||||||
|
});
|
||||||
|
}, 150);
|
||||||
|
}
|
||||||
|
|
||||||
|
const onEndOpenChange = (open: boolean) => {
|
||||||
|
if (open) {
|
||||||
|
setEndOpen(true);
|
||||||
|
if (endTime) {
|
||||||
|
const endTimeArray = endTime.split(' ');
|
||||||
|
setTimeout(() => {
|
||||||
|
const endAmPmList = document.querySelectorAll('#endAmPm button');
|
||||||
|
const targetAmPm = Array.from(endAmPmList).find(button => {
|
||||||
|
return button.textContent.trim() === endTimeArray[0];
|
||||||
|
});
|
||||||
|
const endHourList = document.querySelectorAll('#endHour button');
|
||||||
|
const targetHour = Array.from(endHourList).find(button => {
|
||||||
|
return button.textContent.trim() === endTimeArray[1].slice(0, 2);
|
||||||
|
});
|
||||||
|
const endMinuteList = document.querySelectorAll('#endMinute button');
|
||||||
|
const targetMinute = Array.from(endMinuteList).find(button => {
|
||||||
|
return button.textContent.trim() === endTimeArray[2].slice(0, 2);
|
||||||
|
});
|
||||||
|
[targetAmPm, targetHour, targetMinute].forEach(element => {
|
||||||
|
(element as HTMLButtonElement).click();
|
||||||
|
});
|
||||||
|
}, 150);
|
||||||
|
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setEndOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const applyEndTime = () => {
|
||||||
|
const endOnStateElements = document.querySelectorAll('[id^="end"] [data-state="on"]');
|
||||||
|
if (endOnStateElements.length < 3) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const {
|
||||||
|
endAmPm,
|
||||||
|
endHour,
|
||||||
|
endMinute
|
||||||
|
} = {
|
||||||
|
endAmPm: document.querySelector('#endAmPm [data-state="on"]')?.textContent.trim(),
|
||||||
|
endHour: document.querySelector('#endHour [data-state="on"]')?.textContent.trim(),
|
||||||
|
endMinute: document.querySelector('#endMinute [data-state="on"]')?.textContent.trim()
|
||||||
|
};
|
||||||
|
const endTime = `${endAmPm} ${endHour}시 ${endMinute}분`;
|
||||||
|
setEndTime(endTime);
|
||||||
|
}
|
||||||
|
const cancelEndTime = () => {
|
||||||
|
setEndOpen(false);
|
||||||
|
setTimeout(() => {
|
||||||
|
const endOnElements = document.querySelectorAll('[id^="end"] [data-state="on"]');
|
||||||
|
endOnElements.forEach(element => {
|
||||||
|
(element as HTMLButtonElement).click();
|
||||||
|
});
|
||||||
|
}, 150);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="w-full h-full flex flex-row justify-around items-center"
|
className="w-full h-full flex flex-row justify-between gap-3 items-center"
|
||||||
>
|
>
|
||||||
<Popover
|
<Popover
|
||||||
|
open={startOpen}
|
||||||
|
onOpenChange={onStartOpenChange}
|
||||||
>
|
>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
className="border border-indigo-100 bg-white hover:bg-indigo-100 text-black"
|
className="flex-9 h-full border border-indigo-100 bg-white hover:bg-indigo-100 text-black"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
시작 시간 설정
|
{
|
||||||
|
!startTime
|
||||||
|
? '시작 시간 설정'
|
||||||
|
: startTime
|
||||||
|
}
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
@@ -55,6 +177,7 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
|
|||||||
side={popoverAlign === 'start' ? 'bottom' : 'top'}
|
side={popoverAlign === 'start' ? 'bottom' : 'top'}
|
||||||
>
|
>
|
||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
|
id="startAmPm"
|
||||||
type="single"
|
type="single"
|
||||||
className="w-15 flex flex-col justify-start items-center"
|
className="w-15 flex flex-col justify-start items-center"
|
||||||
>
|
>
|
||||||
@@ -76,6 +199,7 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
|
|||||||
>
|
>
|
||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
type="single"
|
type="single"
|
||||||
|
id="startHour"
|
||||||
className="w-15 border-r rounded-none border-l flex flex-col justify-start items-center"
|
className="w-15 border-r rounded-none border-l flex flex-col justify-start items-center"
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
@@ -83,6 +207,7 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
|
|||||||
<ToggleGroupItem
|
<ToggleGroupItem
|
||||||
className="w-full h-7 rounded-none!"
|
className="w-full h-7 rounded-none!"
|
||||||
value={time.toString().padStart(2, '0')}
|
value={time.toString().padStart(2, '0')}
|
||||||
|
key={`startHour${time.toString().padStart(2, '0')}`}
|
||||||
>
|
>
|
||||||
{time.toString().padStart(2, '0')}
|
{time.toString().padStart(2, '0')}
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
@@ -95,6 +220,7 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
|
|||||||
>
|
>
|
||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
type="single"
|
type="single"
|
||||||
|
id="startMinute"
|
||||||
className="w-full h-full rounded-none border-r flex flex-col justify-start items-center"
|
className="w-full h-full rounded-none border-r flex flex-col justify-start items-center"
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
@@ -102,6 +228,7 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
|
|||||||
<ToggleGroupItem
|
<ToggleGroupItem
|
||||||
value={idx.toString().padStart(2, '0')}
|
value={idx.toString().padStart(2, '0')}
|
||||||
className="w-full h-7 rounded-none!"
|
className="w-full h-7 rounded-none!"
|
||||||
|
key={`startMinute${idx.toString().padStart(2, '0')}`}
|
||||||
>
|
>
|
||||||
{idx.toString().padStart(2, '0')}
|
{idx.toString().padStart(2, '0')}
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
@@ -112,26 +239,34 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
|
|||||||
<div className="w-15 h-full flex flex-col">
|
<div className="w-15 h-full flex flex-col">
|
||||||
<div
|
<div
|
||||||
className="cursor-default text-sm flex justify-center items-center w-full h-7 rounded-none rounded-tr-md! hover:bg-gray-100"
|
className="cursor-default text-sm flex justify-center items-center w-full h-7 rounded-none rounded-tr-md! hover:bg-gray-100"
|
||||||
|
onClick={applyStartTime}
|
||||||
>
|
>
|
||||||
적용
|
적용
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="cursor-default text-sm flex justify-center items-center w-full h-7 rounded-none rounded-tr-md! hover:bg-gray-100"
|
className="cursor-default text-sm flex justify-center items-center w-full h-7 rounded-none rounded-tr-md! hover:bg-gray-100"
|
||||||
|
onClick={cancelStartTime}
|
||||||
>
|
>
|
||||||
취소
|
취소
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
<span> - </span>
|
<div className="flex-2 h-px bg-indigo-300" />
|
||||||
<Popover
|
<Popover
|
||||||
|
open={endOpen}
|
||||||
|
onOpenChange={onEndOpenChange}
|
||||||
>
|
>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
className="border border-indigo-100 bg-white hover:bg-indigo-100 text-black"
|
className="flex-9 h-full border border-indigo-100 bg-white hover:bg-indigo-100 text-black"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
종료 시간 설정
|
{
|
||||||
|
!endTime
|
||||||
|
? '종료 시간 설정'
|
||||||
|
: `${endTime}`
|
||||||
|
}
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
@@ -140,6 +275,7 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
|
|||||||
side={popoverAlign === 'start' ? 'bottom' : 'top'}
|
side={popoverAlign === 'start' ? 'bottom' : 'top'}
|
||||||
>
|
>
|
||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
|
id="endAmPm"
|
||||||
type="single"
|
type="single"
|
||||||
className="w-15 flex flex-col justify-start items-center"
|
className="w-15 flex flex-col justify-start items-center"
|
||||||
>
|
>
|
||||||
@@ -160,6 +296,7 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
|
|||||||
className="w-15 h-full"
|
className="w-15 h-full"
|
||||||
>
|
>
|
||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
|
id="endHour"
|
||||||
type="single"
|
type="single"
|
||||||
className="w-15 border-r rounded-none border-l flex flex-col justify-start items-center"
|
className="w-15 border-r rounded-none border-l flex flex-col justify-start items-center"
|
||||||
>
|
>
|
||||||
@@ -167,6 +304,7 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
|
|||||||
[1,2,3,4,5,6,7,8,9,10,11,12].map((time) => (
|
[1,2,3,4,5,6,7,8,9,10,11,12].map((time) => (
|
||||||
<ToggleGroupItem
|
<ToggleGroupItem
|
||||||
className="w-full h-7 rounded-none!"
|
className="w-full h-7 rounded-none!"
|
||||||
|
key={`endHour${time.toString().padStart(2, '0')}`}
|
||||||
value={time.toString().padStart(2, '0')}
|
value={time.toString().padStart(2, '0')}
|
||||||
>
|
>
|
||||||
{time.toString().padStart(2, '0')}
|
{time.toString().padStart(2, '0')}
|
||||||
@@ -179,6 +317,7 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
|
|||||||
className="w-15 h-full"
|
className="w-15 h-full"
|
||||||
>
|
>
|
||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
|
id="endMinute"
|
||||||
type="single"
|
type="single"
|
||||||
className="w-full h-full rounded-none border-r flex flex-col justify-start items-center"
|
className="w-full h-full rounded-none border-r flex flex-col justify-start items-center"
|
||||||
>
|
>
|
||||||
@@ -186,6 +325,7 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
|
|||||||
Array.from({ length: 60 }).map((_, idx) => (
|
Array.from({ length: 60 }).map((_, idx) => (
|
||||||
<ToggleGroupItem
|
<ToggleGroupItem
|
||||||
value={idx.toString().padStart(2, '0')}
|
value={idx.toString().padStart(2, '0')}
|
||||||
|
key={`endMinute${idx.toString().padStart(2, '0')}`}
|
||||||
className="w-full h-7 rounded-none!"
|
className="w-full h-7 rounded-none!"
|
||||||
>
|
>
|
||||||
{idx.toString().padStart(2, '0')}
|
{idx.toString().padStart(2, '0')}
|
||||||
@@ -197,11 +337,13 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
|
|||||||
<div className="w-15 h-full flex flex-col">
|
<div className="w-15 h-full flex flex-col">
|
||||||
<div
|
<div
|
||||||
className="cursor-default text-sm flex justify-center items-center w-full h-7 rounded-none rounded-tr-md! hover:bg-gray-100"
|
className="cursor-default text-sm flex justify-center items-center w-full h-7 rounded-none rounded-tr-md! hover:bg-gray-100"
|
||||||
|
onClick={applyEndTime}
|
||||||
>
|
>
|
||||||
적용
|
확인
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="cursor-default text-sm flex justify-center items-center w-full h-7 rounded-none rounded-tr-md! hover:bg-gray-100"
|
className="cursor-default text-sm flex justify-center items-center w-full h-7 rounded-none rounded-tr-md! hover:bg-gray-100"
|
||||||
|
onClick={cancelEndTime}
|
||||||
>
|
>
|
||||||
취소
|
취소
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
13
src/ui/component/popover/schedule/content/ContentProps.ts
Normal file
13
src/ui/component/popover/schedule/content/ContentProps.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import type { ColorPaletteType } from "@/const/ColorPalette";
|
||||||
|
|
||||||
|
interface BaseProps {
|
||||||
|
date: Date | undefined;
|
||||||
|
open: boolean;
|
||||||
|
popoverSide: 'left' | 'right';
|
||||||
|
popoverAlign: 'start' | 'end';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScheduleCreateContentProps extends BaseProps {
|
||||||
|
date: Date | undefined;
|
||||||
|
setMode: (mode: 'list' | 'create' | 'detail' | 'update') => void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { usePalette } from '@/hooks/use-palette';
|
||||||
|
import { type ColorPaletteType } from '@/const/ColorPalette';
|
||||||
|
import { ColorPickPopover } from '../ColorPickPopover';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
import * as z from 'zod';
|
||||||
|
import { CreateScheduleSchema } from '@/data/form/createSchedule.schema';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { Field, FieldError } from '@/components/ui/field';
|
||||||
|
import { ArrowLeft, PenSquare, X, XIcon } from 'lucide-react';
|
||||||
|
import { ScheduleTypeLabel, type ScheduleType } from '@/const/schedule/ScheduleType';
|
||||||
|
import { useRecord } from '@/hooks/use-record';
|
||||||
|
import { TypePickPopover } from '../TypePickPopover';
|
||||||
|
import { ScheduleDay } from '@/const/schedule/ScheduleDay';
|
||||||
|
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
||||||
|
import { DatePickPopover } from '../DatePickPopover';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { TimePickPopover } from '../TimePickPopover';
|
||||||
|
import { useTime } from '@/hooks/use-time';
|
||||||
|
import type { ScheduleCreateContentProps } from './ContentProps';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { ParticipantPopover } from '../ParticipantPopover';
|
||||||
|
|
||||||
|
export const ScheduleCreateContent = ({ date, open, setMode, popoverSide, popoverAlign }: ScheduleCreateContentProps) => {
|
||||||
|
const [colorPopoverOpen, setColorPopoverOpen] = useState(false);
|
||||||
|
const { getPaletteByKey } = usePalette();
|
||||||
|
const { getNowString } = useTime();
|
||||||
|
const dayLabelList = useRecord(ScheduleDay).keys.map((key) => {
|
||||||
|
return {
|
||||||
|
day: Number(key),
|
||||||
|
label: ScheduleDay[Number(key)]
|
||||||
|
} as { day: number, label: string };
|
||||||
|
})
|
||||||
|
|
||||||
|
const createScheduleForm = useForm<z.infer<typeof CreateScheduleSchema>>({
|
||||||
|
resolver: zodResolver(CreateScheduleSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: "",
|
||||||
|
startDate: date || new Date(),
|
||||||
|
endDate: date || new Date(),
|
||||||
|
content: "",
|
||||||
|
startTime: getNowString(),
|
||||||
|
endTime: getNowString(),
|
||||||
|
type: "once",
|
||||||
|
status: "yet",
|
||||||
|
style: getPaletteByKey('Black').style,
|
||||||
|
dayList: "",
|
||||||
|
participantList: []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
content,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
type,
|
||||||
|
status,
|
||||||
|
style,
|
||||||
|
dayList
|
||||||
|
} = createScheduleForm.watch();
|
||||||
|
|
||||||
|
const selectColor = (color: ColorPaletteType) => {
|
||||||
|
createScheduleForm.setValue('style', color.style);
|
||||||
|
setColorPopoverOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectType = (type: ScheduleType) => {
|
||||||
|
createScheduleForm.setValue('type', type);
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectDayList = (newValues: string[]) => {
|
||||||
|
const sortedValues = newValues.sort();
|
||||||
|
const newDayList = sortedValues.join('');
|
||||||
|
createScheduleForm.setValue('dayList', newDayList);
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectDate = (type: 'startDate' | 'endDate', date: Date | undefined) => {
|
||||||
|
if (!date) return;
|
||||||
|
createScheduleForm.setValue(type, date);
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectTime = (type: 'startTime' | 'endTime', time: string) => {
|
||||||
|
createScheduleForm.setValue(type, time);
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectParticipant = (participantList: string[]) => {
|
||||||
|
createScheduleForm.setValue('participantList', participantList);
|
||||||
|
}
|
||||||
|
|
||||||
|
const OnceContent = () => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="w-full h-10"
|
||||||
|
>
|
||||||
|
<DatePickPopover
|
||||||
|
disabled={false}
|
||||||
|
mode={'range'}
|
||||||
|
popoverAlign={popoverAlign}
|
||||||
|
startDate={startDate}
|
||||||
|
endDate={endDate}
|
||||||
|
setStartDate={(date: Date | undefined) => selectDate('startDate', date)}
|
||||||
|
setEndDate={(date: Date | undefined) => selectDate('endDate', date)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const DailyContent = () => {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DefaultContent = () => {
|
||||||
|
return (
|
||||||
|
(
|
||||||
|
<ToggleGroup
|
||||||
|
type="multiple"
|
||||||
|
className="w-full h-10 flex flex-row justify-center items-center"
|
||||||
|
onValueChange={selectDayList}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
dayLabelList.map((day) => (
|
||||||
|
<ToggleGroupItem
|
||||||
|
value={`${day.day}`}
|
||||||
|
className="border w-[calc(100%/7)] h-10 rounded-none not-last:border-r-0"
|
||||||
|
>
|
||||||
|
{day.label}
|
||||||
|
</ToggleGroupItem>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</ToggleGroup>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderContent = () => {
|
||||||
|
switch (type) {
|
||||||
|
case 'once':
|
||||||
|
return <OnceContent />;
|
||||||
|
case 'daily':
|
||||||
|
return <DailyContent />;
|
||||||
|
default:
|
||||||
|
return <DefaultContent />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex flex-col justify-start items-start gap-4">
|
||||||
|
<div className="w-full flex flex-row justify-between items-center gap-4">
|
||||||
|
<div
|
||||||
|
onClick={() => setMode('list')}
|
||||||
|
>
|
||||||
|
<ArrowLeft />
|
||||||
|
</div>
|
||||||
|
<Controller
|
||||||
|
name="name"
|
||||||
|
control={createScheduleForm.control}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<Field data-invalid={fieldState.invalid}>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
id="form-create-schedule-name"
|
||||||
|
placeholder="제목"
|
||||||
|
className="font-bold border-t-0 border-r-0 border-l-0 p-0 border-b-2 rounded-none shadow-none border-indigo-100 focus-visible:ring-0 focus-visible:border-b-indigo-300"
|
||||||
|
style={{
|
||||||
|
fontSize: '20px'
|
||||||
|
}}
|
||||||
|
tabIndex={1}
|
||||||
|
arai-invalid={fieldState.invalid}
|
||||||
|
/>
|
||||||
|
<FieldError errors={[fieldState.error]} />
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Popover open={colorPopoverOpen} onOpenChange={setColorPopoverOpen}>
|
||||||
|
<div className="w-6 h-6 flex justify-center items-center">
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'rounded-full w-5 h-5 border-2 border-gray-300',
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
backgroundColor: `${style}`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</PopoverTrigger>
|
||||||
|
</div>
|
||||||
|
<ColorPickPopover
|
||||||
|
setColor={selectColor}
|
||||||
|
/>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<div className="hover:bg-indigo-100 cursor-default w-full h-10 border border-indigo-100 flex justify-center items-center rounded-sm">
|
||||||
|
{ScheduleTypeLabel[type as keyof typeof ScheduleTypeLabel]}
|
||||||
|
</div>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<TypePickPopover
|
||||||
|
setType={selectType}
|
||||||
|
popoverSide={popoverSide}
|
||||||
|
/>
|
||||||
|
</Popover>
|
||||||
|
<div
|
||||||
|
className="w-full h-10"
|
||||||
|
>
|
||||||
|
{renderContent()}
|
||||||
|
</div>
|
||||||
|
<div className="w-full h-10">
|
||||||
|
<TimePickPopover
|
||||||
|
mode='range'
|
||||||
|
popoverAlign={popoverAlign}
|
||||||
|
disabled={false}
|
||||||
|
startTime={startTime}
|
||||||
|
setStartTime={(time: string | undefined) => selectTime('startTime', time ?? '')}
|
||||||
|
endTime={endTime}
|
||||||
|
setEndTime={(time: string | undefined) => selectTime('endTime', time ?? '')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Controller
|
||||||
|
name="content"
|
||||||
|
control={createScheduleForm.control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Textarea
|
||||||
|
{...field}
|
||||||
|
rows={2}
|
||||||
|
placeholder="일정 상세 사항"
|
||||||
|
className="placeholder-gray-300! focus-visible:placeholder-gray-400! border-indigo-100 focus-visible:border-indigo-300 resize-none focus-visible:ring-0"
|
||||||
|
style={{
|
||||||
|
'scrollbarWidth': 'none'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<ParticipantPopover />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user