Compare commits
5 Commits
ea7861b63a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c8dcf9db7 | |||
|
|
4a8e761b3d | ||
| 0c8e0893c7 | |||
|
|
7df60fe004 | ||
| daab622638 |
@@ -10,6 +10,7 @@ import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { Button, buttonVariants } from "@/components/ui/button"
|
import { Button, buttonVariants } from "@/components/ui/button"
|
||||||
|
import { format } from "date-fns"
|
||||||
|
|
||||||
function Calendar({
|
function Calendar({
|
||||||
className,
|
className,
|
||||||
@@ -36,8 +37,28 @@ function Calendar({
|
|||||||
)}
|
)}
|
||||||
captionLayout={captionLayout}
|
captionLayout={captionLayout}
|
||||||
formatters={{
|
formatters={{
|
||||||
|
formatCaption: (month) => format(month, "yyyy년 MM월"),
|
||||||
|
formatWeekdayName: (weekday) => {
|
||||||
|
switch(weekday.getDay()) {
|
||||||
|
case 0:
|
||||||
|
return '일';
|
||||||
|
case 1:
|
||||||
|
return '월';
|
||||||
|
case 2:
|
||||||
|
return '화';
|
||||||
|
case 3:
|
||||||
|
return '수';
|
||||||
|
case 4:
|
||||||
|
return '목';
|
||||||
|
case 5:
|
||||||
|
return '금';
|
||||||
|
case 6:
|
||||||
|
return '토';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
formatMonthDropdown: (date) =>
|
formatMonthDropdown: (date) =>
|
||||||
date.toLocaleString("default", { month: "short" }),
|
date.toLocaleString("", { month: "short" }),
|
||||||
...formatters,
|
...formatters,
|
||||||
}}
|
}}
|
||||||
classNames={{
|
classNames={{
|
||||||
|
|||||||
99
src/const/ColorPalette.ts
Normal file
99
src/const/ColorPalette.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
export type ColorPaletteType = {
|
||||||
|
index: number;
|
||||||
|
style: string;
|
||||||
|
main: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const ColorPalette: Record<any, ColorPaletteType> = {
|
||||||
|
Black: {
|
||||||
|
index: 0,
|
||||||
|
style: '#000000',
|
||||||
|
main: true
|
||||||
|
},
|
||||||
|
White: {
|
||||||
|
index: 1,
|
||||||
|
style: '#FFFFFF',
|
||||||
|
main: true
|
||||||
|
},
|
||||||
|
PeachCream: {
|
||||||
|
index: 2,
|
||||||
|
style: '#FFDAB9',
|
||||||
|
main: false
|
||||||
|
},
|
||||||
|
CoralPink: {
|
||||||
|
index: 3,
|
||||||
|
style: '#F08080',
|
||||||
|
main: true
|
||||||
|
},
|
||||||
|
MintIcing: {
|
||||||
|
index: 4,
|
||||||
|
style: '#C1E1C1',
|
||||||
|
main: false
|
||||||
|
},
|
||||||
|
Vanilla: {
|
||||||
|
index: 5,
|
||||||
|
style: '#FFFACD',
|
||||||
|
main: true
|
||||||
|
},
|
||||||
|
Wheat: {
|
||||||
|
index: 6,
|
||||||
|
style: '#F5DEB3',
|
||||||
|
main: false
|
||||||
|
},
|
||||||
|
AliceBlue: {
|
||||||
|
index: 7,
|
||||||
|
style: '#F0F8FF',
|
||||||
|
main: true
|
||||||
|
},
|
||||||
|
Lavender: {
|
||||||
|
index: 8,
|
||||||
|
style: '#E6E6FA',
|
||||||
|
main: false
|
||||||
|
},
|
||||||
|
LightAqua: {
|
||||||
|
index: 9,
|
||||||
|
style: '#A8E6CF',
|
||||||
|
main: true
|
||||||
|
},
|
||||||
|
CloudWhite: {
|
||||||
|
index: 10,
|
||||||
|
style: '#F0F8FF',
|
||||||
|
main: false
|
||||||
|
},
|
||||||
|
LightGray: {
|
||||||
|
index: 11,
|
||||||
|
style: '#D3D3D3',
|
||||||
|
main: true
|
||||||
|
},
|
||||||
|
LightKhakki: {
|
||||||
|
index: 12,
|
||||||
|
style: '#F0F8E6',
|
||||||
|
main: false
|
||||||
|
},
|
||||||
|
DustyRose: {
|
||||||
|
index: 13,
|
||||||
|
style: '#D8BFD8',
|
||||||
|
main: true
|
||||||
|
},
|
||||||
|
CreamBeige: {
|
||||||
|
index: 14,
|
||||||
|
style: '#FAF0E6',
|
||||||
|
main: true,
|
||||||
|
},
|
||||||
|
Oatmeal: {
|
||||||
|
index: 15,
|
||||||
|
style: '#FDF5E6',
|
||||||
|
main: false
|
||||||
|
},
|
||||||
|
CharcoalLight: {
|
||||||
|
index: 16,
|
||||||
|
style: '#A9A9A9',
|
||||||
|
main: true
|
||||||
|
},
|
||||||
|
Custom: {
|
||||||
|
index: 17,
|
||||||
|
style: 'transparent',
|
||||||
|
main: false
|
||||||
|
},
|
||||||
|
}
|
||||||
67
src/hooks/use-palette.ts
Normal file
67
src/hooks/use-palette.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { ColorPalette, type ColorPaletteType } from "@/const/ColorPalette";
|
||||||
|
|
||||||
|
export function usePalette() {
|
||||||
|
const ColorPaletteType = typeof ColorPalette;
|
||||||
|
|
||||||
|
const getPaletteNameList = () => {
|
||||||
|
return Object.keys(ColorPalette);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMainPaletteList = () => {
|
||||||
|
const paletteKeys = Object.keys(ColorPalette);
|
||||||
|
let paletteList: ColorPaletteType[] = [];
|
||||||
|
paletteKeys.forEach((paletteKey) => {
|
||||||
|
const key = paletteKey as keyof typeof ColorPalette;
|
||||||
|
const palette: ColorPaletteType = ColorPalette[key];
|
||||||
|
if (palette.main) {
|
||||||
|
paletteList.push(palette);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
paletteList = paletteList.sort((a, b) => a.index - b.index);
|
||||||
|
|
||||||
|
return paletteList;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getExtraPaletteList = () => {
|
||||||
|
const paletteKeys = Object.keys(ColorPalette);
|
||||||
|
let paletteList: ColorPaletteType[] = [];
|
||||||
|
paletteKeys.forEach((paletteKey) => {
|
||||||
|
const key = paletteKey as keyof typeof ColorPalette;
|
||||||
|
const palette: ColorPaletteType = ColorPalette[key];
|
||||||
|
if (!palette.main) {
|
||||||
|
paletteList.push(palette);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
paletteList = paletteList.sort((a, b) => a.index - b.index);
|
||||||
|
return paletteList;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAllPaletteList = [...getMainPaletteList(), ...getExtraPaletteList()].sort((a, b) => a.index - b.index);
|
||||||
|
|
||||||
|
const getPaletteByKey = (key: keyof typeof ColorPalette) => {
|
||||||
|
return ColorPalette[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCustomColor = (style: string) => {
|
||||||
|
return {
|
||||||
|
style: `#${style}`,
|
||||||
|
main: false
|
||||||
|
} as ColorPaletteType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyle = (palette: ColorPaletteType) => {
|
||||||
|
return palette.style;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ColorPaletteType,
|
||||||
|
getPaletteNameList,
|
||||||
|
getMainPaletteList,
|
||||||
|
getExtraPaletteList,
|
||||||
|
getAllPaletteList,
|
||||||
|
getPaletteByKey,
|
||||||
|
getCustomColor,
|
||||||
|
getStyle
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/hooks/use-viewport.ts
Normal file
27
src/hooks/use-viewport.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
const useViewport = () => {
|
||||||
|
const [width, setWidth] = useState(
|
||||||
|
typeof window !== 'undefined' ? window.innerWidth : 0
|
||||||
|
);
|
||||||
|
const [height, setHeight] = useState(
|
||||||
|
typeof window !== 'undefined' ? window.innerHeight : 0
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleResize = () => {
|
||||||
|
setWidth(window.innerWidth);
|
||||||
|
setHeight(window.innerHeight);
|
||||||
|
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { width, height };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useViewport;
|
||||||
@@ -124,6 +124,7 @@ html, body, #root {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
min-width: 1280px;
|
min-width: 1280px;
|
||||||
min-height: 720px;
|
min-height: 720px;
|
||||||
|
max-height: 1080px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Chrome, Safari, Edge */
|
/* Chrome, Safari, Edge */
|
||||||
@@ -133,7 +134,19 @@ input[type="number"]::-webkit-outer-spin-button {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rdp-day {
|
||||||
|
aspect-ratio: unset;
|
||||||
|
}
|
||||||
|
|
||||||
/* Firefox */
|
/* Firefox */
|
||||||
input[type="number"] {
|
input[type="number"] {
|
||||||
-moz-appearance: textfield;
|
-moz-appearance: textfield;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rdp-week:not(:first-child) {
|
||||||
|
@apply border-t;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rdp-day:not(:first-child) {
|
||||||
|
@apply border-l;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import SideBar from "@/ui/component/SideBar";
|
import SideBar from "@/ui/component/SideBar";
|
||||||
import { Outlet } from "react-router-dom";
|
import { Outlet, useNavigate } from "react-router-dom";
|
||||||
import { SidebarProvider } from "@/components/ui/sidebar";
|
import { SidebarProvider } from "@/components/ui/sidebar";
|
||||||
import Header from "@/ui/component/Header";
|
import Header from "@/ui/component/Header";
|
||||||
import { useAuthStore } from '@/store/authStore';
|
import { useAuthStore } from '@/store/authStore';
|
||||||
@@ -11,9 +11,22 @@ import {
|
|||||||
OctagonXIcon,
|
OctagonXIcon,
|
||||||
TriangleAlertIcon,
|
TriangleAlertIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
export default function Layout() {
|
export default function Layout() {
|
||||||
const { authData } = useAuthStore();
|
const { authData } = useAuthStore();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const pathname = location.pathname;
|
||||||
|
|
||||||
|
const goTo = (path: string) => {
|
||||||
|
console.log(path);
|
||||||
|
console.log(pathname);
|
||||||
|
if (path === pathname) return;
|
||||||
|
navigate(path);
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Toaster
|
<Toaster
|
||||||
@@ -27,10 +40,11 @@ export default function Layout() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<SidebarProvider
|
<SidebarProvider
|
||||||
defaultOpen={false}
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
id="root"
|
id="root"
|
||||||
>
|
>
|
||||||
<SideBar />
|
<SideBar goTo={goTo} />
|
||||||
<div className="flex flex-col w-full h-full">
|
<div className="flex flex-col w-full h-full">
|
||||||
{ authData ? <Header /> : null}
|
{ authData ? <Header /> : null}
|
||||||
<div className="w-full h-full p-2.5">
|
<div className="w-full h-full p-2.5">
|
||||||
|
|||||||
@@ -5,11 +5,20 @@ import {
|
|||||||
SidebarFooter,
|
SidebarFooter,
|
||||||
SidebarHeader
|
SidebarHeader
|
||||||
} from '@/components/ui/sidebar';
|
} from '@/components/ui/sidebar';
|
||||||
|
import { PageRouting } from '@/const/PageRouting';
|
||||||
|
|
||||||
|
interface SideBarProps {
|
||||||
|
goTo: (path: string) => void;
|
||||||
|
}
|
||||||
|
export default function SideBar({ goTo } : SideBarProps) {
|
||||||
|
|
||||||
export default function SideBar() {
|
|
||||||
return (
|
return (
|
||||||
<Sidebar forceSheet={true}>
|
<Sidebar forceSheet={true}>
|
||||||
|
<SidebarHeader></SidebarHeader>
|
||||||
|
<SidebarContent className="flex flex-col p-4 cursor-default">
|
||||||
|
<div onClick={() => goTo(PageRouting["HOME"].path)}>Home</div>
|
||||||
|
<div onClick={() => goTo(PageRouting["SCHEDULES"].path)}>Schedules</div>
|
||||||
|
</SidebarContent>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
190
src/ui/component/calendar/CustomCalendar.tsx
Normal file
190
src/ui/component/calendar/CustomCalendar.tsx
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Calendar } from "@/components/ui/calendar";
|
||||||
|
import { useLayoutEffect, useRef, useState } from "react";
|
||||||
|
import { getDefaultClassNames } from "react-day-picker";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import { isSameDay, getWeeksInMonth, getWeekOfMonth } from "date-fns";
|
||||||
|
import { SchedulePopover } from "../popover/SchedulePopover";
|
||||||
|
|
||||||
|
interface CustomCalendarProps {
|
||||||
|
data?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CustomCalendar = ({ data }: CustomCalendarProps) => {
|
||||||
|
const [weekCount, setWeekCount] = useState(5);
|
||||||
|
const [selectedDate, setSelectedDate] = useState<Date | undefined>(undefined);
|
||||||
|
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||||
|
const [popoverSide, setPopoverSide] = useState<'right' | 'left'>('right');
|
||||||
|
const [popoverAlign, setPopoverAlign] = useState<'start' | 'end'>('end');
|
||||||
|
const defaultClassNames = getDefaultClassNames();
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const updateWeekCount = () => {
|
||||||
|
if (containerRef === null) return;
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
|
||||||
|
const weeks = containerRef.current.querySelectorAll('.rdp-week');
|
||||||
|
|
||||||
|
if (weeks?.length) setWeekCount(weeks.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
updateWeekCount();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleOpenChange = (open: boolean) => {
|
||||||
|
setPopoverOpen(open);
|
||||||
|
if (!open) {
|
||||||
|
setTimeout(() => {
|
||||||
|
setSelectedDate(undefined);
|
||||||
|
}, 150);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDaySelect = (date: Date | undefined) => {
|
||||||
|
if (!date) {
|
||||||
|
setPopoverOpen(false);
|
||||||
|
setTimeout(() => {
|
||||||
|
setSelectedDate(undefined);
|
||||||
|
}, 150);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (date) {
|
||||||
|
setSelectedDate(date);
|
||||||
|
|
||||||
|
const dayOfWeek = date.getDay();
|
||||||
|
|
||||||
|
if (0 <= dayOfWeek && dayOfWeek < 4) {
|
||||||
|
setPopoverSide('right');
|
||||||
|
} else {
|
||||||
|
setPopoverSide('left');
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = { weekStartsOn: 0 as 0 };
|
||||||
|
|
||||||
|
const totalWeeks = getWeeksInMonth(date, options);
|
||||||
|
|
||||||
|
const currentWeekNumber = getWeekOfMonth(date, options);
|
||||||
|
|
||||||
|
const threshold = Math.ceil(totalWeeks / 2);
|
||||||
|
|
||||||
|
if (currentWeekNumber <= threshold) {
|
||||||
|
setPopoverAlign('start');
|
||||||
|
} else {
|
||||||
|
setPopoverAlign('end');
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setPopoverOpen(true);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="w-full h-full"
|
||||||
|
ref={containerRef}
|
||||||
|
>
|
||||||
|
<Popover
|
||||||
|
open={popoverOpen}
|
||||||
|
onOpenChange={handleOpenChange}
|
||||||
|
>
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
className="h-full w-full border rounded-lg"
|
||||||
|
selected={selectedDate}
|
||||||
|
onSelect={handleDaySelect}
|
||||||
|
onMonthChange={() => {
|
||||||
|
// month 바뀐 직후 DOM 변화가 생기므로 다음 프레임에서 계산
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
updateWeekCount();
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
classNames={{
|
||||||
|
months: cn(
|
||||||
|
defaultClassNames.months,
|
||||||
|
"w-full h-full relative"
|
||||||
|
),
|
||||||
|
nav: cn(
|
||||||
|
defaultClassNames.nav,
|
||||||
|
"flex w-full item-center gap-1 justify-around absolute top-0 inset-x-0"
|
||||||
|
),
|
||||||
|
month: cn(
|
||||||
|
defaultClassNames.month,
|
||||||
|
"h-full w-full flex flex-col"
|
||||||
|
),
|
||||||
|
month_grid: cn(
|
||||||
|
defaultClassNames.month_grid,
|
||||||
|
"w-full h-full flex-1"
|
||||||
|
),
|
||||||
|
weeks: cn(
|
||||||
|
defaultClassNames.weeks,
|
||||||
|
"w-full h-full"
|
||||||
|
),
|
||||||
|
weekdays: cn(
|
||||||
|
defaultClassNames.weekdays,
|
||||||
|
"w-full"
|
||||||
|
),
|
||||||
|
week: cn(
|
||||||
|
defaultClassNames.week,
|
||||||
|
`w-full`
|
||||||
|
),
|
||||||
|
day: cn(
|
||||||
|
defaultClassNames.day,
|
||||||
|
`w-[calc(100%/7)] rounded-none`
|
||||||
|
),
|
||||||
|
day_button: cn(
|
||||||
|
defaultClassNames.day_button,
|
||||||
|
"h-full w-full flex p-2 justify-start items-start",
|
||||||
|
"hover:bg-transparent",
|
||||||
|
"data-[selected-single=true]:bg-transparent data-[selected-single=true]:text-black"
|
||||||
|
),
|
||||||
|
selected: cn(
|
||||||
|
defaultClassNames.selected,
|
||||||
|
"h-full border-0 fill-transparent"
|
||||||
|
),
|
||||||
|
today: cn(
|
||||||
|
defaultClassNames.today,
|
||||||
|
"h-full"
|
||||||
|
),
|
||||||
|
|
||||||
|
}}
|
||||||
|
styles={{
|
||||||
|
day: {
|
||||||
|
height: `calc(100%/${weekCount})`
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
components={{
|
||||||
|
Day: ({ day, ...props }) => {
|
||||||
|
const date = day.date;
|
||||||
|
const isSelected = selectedDate && isSameDay(selectedDate, date);
|
||||||
|
return (
|
||||||
|
<td {...props}>
|
||||||
|
{ isSelected
|
||||||
|
? <PopoverTrigger asChild>
|
||||||
|
{props.children}
|
||||||
|
</PopoverTrigger>
|
||||||
|
: props.children
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
DayButton: ({ day, ...props}) => (
|
||||||
|
<button
|
||||||
|
{...props}
|
||||||
|
disabled={day.outside}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<SchedulePopover
|
||||||
|
date={selectedDate}
|
||||||
|
popoverSide={popoverSide}
|
||||||
|
popoverAlign={popoverAlign}
|
||||||
|
/>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
72
src/ui/component/popover/ColorPickPopover.tsx
Normal file
72
src/ui/component/popover/ColorPickPopover.tsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { PopoverContent } from "@/components/ui/popover"
|
||||||
|
import type { ColorPaletteType } from "@/const/ColorPalette"
|
||||||
|
import { usePalette } from "@/hooks/use-palette";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
interface ColorPickPopoverProps {
|
||||||
|
setColor: (color: ColorPaletteType) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ColorPickPopover = ({ setColor }: ColorPickPopoverProps) => {
|
||||||
|
const [seeMore, setSeeMore] = useState(false);
|
||||||
|
const {
|
||||||
|
getMainPaletteList,
|
||||||
|
getExtraPaletteList,
|
||||||
|
getCustomColor
|
||||||
|
} = usePalette();
|
||||||
|
const mainPaletteList = getMainPaletteList();
|
||||||
|
const extraPaletteList = getExtraPaletteList();
|
||||||
|
|
||||||
|
const getSlicedList = (paletteList: ColorPaletteType[], length: number) => {
|
||||||
|
const slicedList: ColorPaletteType[][] = [];
|
||||||
|
let index = 0;
|
||||||
|
while (index < paletteList.length) {
|
||||||
|
slicedList.push(paletteList.slice(index, index + length));
|
||||||
|
index += length;
|
||||||
|
}
|
||||||
|
return slicedList;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PopoverContent
|
||||||
|
className="flex flex-col gap-1.5 w-fit"
|
||||||
|
>
|
||||||
|
{getSlicedList(mainPaletteList, 5).map((list) => (
|
||||||
|
<div className="flex flex-row gap-2.5">
|
||||||
|
{list.map((palette) => (
|
||||||
|
<div
|
||||||
|
className="rounded-full w-5 h-5 border border-gray-300"
|
||||||
|
style={{ backgroundColor: `${palette.style}` }}
|
||||||
|
onClick={() => setColor(palette)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{
|
||||||
|
!seeMore
|
||||||
|
? <div className="w-full" onClick={() => setSeeMore(true)}>더 보기</div>
|
||||||
|
: <>
|
||||||
|
{getSlicedList(extraPaletteList, 5).map((list) => (
|
||||||
|
<div className="flex flex-row gap-2.5">
|
||||||
|
{list.map((palette) => (
|
||||||
|
<div
|
||||||
|
className="rounded-full w-5 h-5 border border-gray-300"
|
||||||
|
style={{
|
||||||
|
backgroundColor: `${palette.style !== 'transparent' && palette.style}`,
|
||||||
|
background: `${palette.style === 'transparent' && 'linear-gradient(135deg, black 50%, white 50%)' }`
|
||||||
|
}}
|
||||||
|
onClick={() => setColor(palette)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
seeMore
|
||||||
|
? <div className="w-full" onClick={() => setSeeMore(false)}>접기</div>
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
</PopoverContent>
|
||||||
|
)
|
||||||
|
}
|
||||||
75
src/ui/component/popover/SchedulePopover.tsx
Normal file
75
src/ui/component/popover/SchedulePopover.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
|
import { Sheet, SheetContent, SheetHeader } from '@/components/ui/sheet';
|
||||||
|
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';
|
||||||
|
|
||||||
|
interface ScheduleSheetProps {
|
||||||
|
date: Date | undefined;
|
||||||
|
popoverSide: 'left' | 'right';
|
||||||
|
popoverAlign: 'start' | 'end';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SchedulePopover = ({ date, popoverSide, popoverAlign }: ScheduleSheetProps) => {
|
||||||
|
const {
|
||||||
|
ColorPaletteType,
|
||||||
|
getPaletteNameList,
|
||||||
|
getMainPaletteList,
|
||||||
|
getAllPaletteList,
|
||||||
|
getCustomColor,
|
||||||
|
getPaletteByKey,
|
||||||
|
getStyle
|
||||||
|
} = usePalette();
|
||||||
|
const defaultColor = getPaletteByKey('Black');
|
||||||
|
const [scheduleColor, setScheduleColor] = useState(defaultColor);
|
||||||
|
const [colorPopoverOpen, setColorPopoverOpen] = useState(false);
|
||||||
|
const selectColor = (color: ColorPaletteType) => {
|
||||||
|
setScheduleColor(color);
|
||||||
|
setColorPopoverOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PopoverContent
|
||||||
|
className="rounded-xl xl:w-[calc(100vw/4)] xl:max-w-[480px] min-w-[320px]"
|
||||||
|
align={popoverAlign} side={popoverSide}
|
||||||
|
>
|
||||||
|
<ScrollArea
|
||||||
|
className={
|
||||||
|
cn(
|
||||||
|
"[&>div>div:last-child]:hidden min-h-[125px] h-[calc(100vh/2)] p-2.5 w-full flex flex-col",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="w-full flex flex-row justify-center items-center gap-4">
|
||||||
|
<Popover open={colorPopoverOpen} onOpenChange={setColorPopoverOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'rounded-full w-5 h-5 border-2 border-gray-300',
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
backgroundColor: `${scheduleColor.style !== 'transparent' && scheduleColor.style}`,
|
||||||
|
background: `${scheduleColor.style === 'transparent' && 'linear-gradient(135deg, black 50%, white 50%)' }`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<ColorPickPopover
|
||||||
|
setColor={selectColor}
|
||||||
|
/>
|
||||||
|
</Popover>
|
||||||
|
<Input
|
||||||
|
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'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</PopoverContent>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ import { useIsMobile } from '@/hooks/use-mobile';
|
|||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
const [autoLogin, setAutoLogin] = useState<boolean>(false);
|
const [autoLogin, setAutoLogin] = useState<boolean>(localStorage.getItem('autoLogin') === 'true');
|
||||||
const { login } = useAuthStore();
|
const { login } = useAuthStore();
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -36,7 +36,7 @@ export default function LoginPage() {
|
|||||||
const { id, password } = { id: loginForm.watch('id'), password: loginForm.watch('password') };
|
const { id, password } = { id: loginForm.watch('id'), password: loginForm.watch('password') };
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem('autoLogin', `${autoLogin}`)
|
localStorage.setItem('autoLogin', `${autoLogin}`);
|
||||||
}, [autoLogin]);
|
}, [autoLogin]);
|
||||||
|
|
||||||
const moveToSignUpPage = useCallback(() => {
|
const moveToSignUpPage = useCallback(() => {
|
||||||
@@ -74,13 +74,19 @@ export default function LoginPage() {
|
|||||||
refreshToken: res.data.refreshToken!
|
refreshToken: res.data.refreshToken!
|
||||||
};
|
};
|
||||||
login({...data});
|
login({...data});
|
||||||
|
if (autoLogin) {
|
||||||
|
localStorage.setItem('auth-storage', JSON.stringify({ state: data }));
|
||||||
|
}
|
||||||
moveToHomePage();
|
moveToHomePage();
|
||||||
return "로그인 성공";
|
return "로그인 성공";
|
||||||
} else {
|
} else {
|
||||||
throw new Error(res.data.message);
|
throw new Error(res.data.message);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (err: Error) => err.message || "에러 발생"
|
error: (err: Error) => {
|
||||||
|
setIsLoading(false);
|
||||||
|
return err.message || "에러 발생"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,11 @@
|
|||||||
import { Calendar } from "@/components/ui/calendar";
|
import { CustomCalendar } from "@/ui/component/calendar/CustomCalendar";
|
||||||
import { DayButton } from "react-day-picker";
|
|
||||||
|
|
||||||
export function ScheduleMainPage() {
|
export function ScheduleMainPage() {
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full flex flex-col justify-start items-center">
|
<div
|
||||||
<Calendar
|
className="w-full h-full p-2"
|
||||||
mode="single"
|
>
|
||||||
className="rounded-lg w-full h-full max-h-10/12 border"
|
<CustomCalendar />
|
||||||
components={{
|
|
||||||
Weeks: (props) => (
|
|
||||||
<tbody {...props} className={props.className}></tbody>
|
|
||||||
),
|
|
||||||
Week: (props) => (
|
|
||||||
<tr {...props} className={props.className + " h-1/10"}></tr>
|
|
||||||
),
|
|
||||||
Day: (props) => (
|
|
||||||
<td {...props} className={props.className + " h-1"}></td>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
|
|
||||||
</Calendar>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ import path from 'path'
|
|||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
server: {
|
server: {
|
||||||
|
host: '0.0.0.0',
|
||||||
port: 5185
|
port: 5185
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
|||||||
Reference in New Issue
Block a user