Created event day modal
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
.counter {
|
||||
/* .counter {
|
||||
font-size: 16px;
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
@@ -181,4 +181,4 @@
|
||||
right: 0;
|
||||
border-right-color: var(--border);
|
||||
}
|
||||
}
|
||||
} */
|
||||
|
||||
@@ -3,13 +3,15 @@ import MonthGrid from './components/MonthGrid'
|
||||
import { buildMonthGrid } from './utils/calendarMath'
|
||||
import type { CalendarEvent, GridLayout } from './types/calendar'
|
||||
import { getEvents } from './lib/api'
|
||||
import DayModal from './components/DayModal'
|
||||
|
||||
|
||||
export default function CalendarPage() {
|
||||
const [visibleMonth, setVisibleMonth] = useState(new Date())
|
||||
const [selectedDate, setSelectedDate] = useState<Date | null>(null)
|
||||
const [events, setEvents] = useState<CalendarEvent[]>([])
|
||||
const [gridWidth, setGridWidth] = useState(0)
|
||||
|
||||
const [isDayModalOpen, setIsDayModalOpen] = useState(false)
|
||||
const gridWrapperRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const days = useMemo(() => buildMonthGrid(visibleMonth), [visibleMonth])
|
||||
@@ -23,6 +25,19 @@ export default function CalendarPage() {
|
||||
})
|
||||
}, [])
|
||||
|
||||
const eventsForSelectedDay = useMemo(() => {
|
||||
if (!selectedDate) return []
|
||||
|
||||
return events.filter((event) => {
|
||||
const d = new Date(event.starts_at)
|
||||
return (
|
||||
d.getFullYear() === selectedDate.getFullYear() &&
|
||||
d.getMonth() === selectedDate.getMonth() &&
|
||||
d.getDate() === selectedDate.getDate()
|
||||
)
|
||||
})
|
||||
}, [events, selectedDate])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!gridWrapperRef.current) return
|
||||
@@ -103,11 +118,33 @@ export default function CalendarPage() {
|
||||
)
|
||||
}
|
||||
|
||||
function goToPreviousDay() {
|
||||
if (!selectedDate) return
|
||||
setSelectedDate(
|
||||
new Date(
|
||||
selectedDate.getFullYear(),
|
||||
selectedDate.getMonth(),
|
||||
selectedDate.getDate() - 1
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function goToNextDay() {
|
||||
if (!selectedDate) return
|
||||
setSelectedDate(
|
||||
new Date(
|
||||
selectedDate.getFullYear(),
|
||||
selectedDate.getMonth(),
|
||||
selectedDate.getDate() + 1
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: '24px',
|
||||
background: 'var(--page-bg)',
|
||||
background: 'var(--surface-page)',
|
||||
color: 'var(--text)',
|
||||
minHeight: '100vh',
|
||||
}}
|
||||
@@ -123,11 +160,29 @@ export default function CalendarPage() {
|
||||
days={days}
|
||||
visibleMonth={visibleMonth}
|
||||
selectedDate={selectedDate}
|
||||
onDayClick={setSelectedDate}
|
||||
onDayClick={(date) => {
|
||||
setSelectedDate(date)
|
||||
setIsDayModalOpen(true)
|
||||
}}
|
||||
events={events}
|
||||
layout={layout}
|
||||
/>
|
||||
</div>
|
||||
<DayModal
|
||||
isOpen={isDayModalOpen}
|
||||
selectedDate={selectedDate}
|
||||
events={eventsForSelectedDay}
|
||||
onClose={() => setIsDayModalOpen(false)}
|
||||
onCreateEvent={() => {
|
||||
console.log('Create event for ISO:', selectedDate?.toISOString())
|
||||
}}
|
||||
onEventClick={(event) => {
|
||||
console.log('Edit event:', event)
|
||||
}}
|
||||
onPreviousDay={goToPreviousDay}
|
||||
onNextDay={goToNextDay}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
@@ -42,8 +42,8 @@ export default function DayCell({
|
||||
style={{
|
||||
position: 'relative',
|
||||
aspectRatio: '1 / 1',
|
||||
border: isSelected ? '2px solid #3b82f6' : '1px solid var(--border)',
|
||||
background: isSelected ? 'rgba(59,130,246,0.15)' : 'var(--card-bg)',
|
||||
border: '1px solid var(--border)',
|
||||
background: 'var(--surface-item)',
|
||||
color: 'var(--text)',
|
||||
borderRadius: '12px',
|
||||
opacity: isCurrentMonth ? 1 : 0.35,
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
import type { CalendarEvent } from '../types/calendar'
|
||||
|
||||
type DayModalProps = {
|
||||
isOpen: boolean
|
||||
selectedDate: Date | null
|
||||
events: CalendarEvent[]
|
||||
onClose: () => void
|
||||
onCreateEvent: () => void
|
||||
onEventClick?: (event: CalendarEvent) => void
|
||||
onPreviousDay: () => void
|
||||
onNextDay: () => void
|
||||
}
|
||||
|
||||
export default function DayModal({
|
||||
isOpen,
|
||||
selectedDate,
|
||||
events,
|
||||
onClose,
|
||||
onCreateEvent,
|
||||
onEventClick,
|
||||
onPreviousDay,
|
||||
onNextDay,
|
||||
}: DayModalProps) {
|
||||
if (!isOpen || !selectedDate) return null
|
||||
|
||||
const sortedEvents = [...events].sort(
|
||||
(a, b) =>
|
||||
new Date(a.starts_at).getTime() - new Date(b.starts_at).getTime()
|
||||
)
|
||||
|
||||
const modalIconButtonStyle: React.CSSProperties = {
|
||||
border: '1px solid var(--border)',
|
||||
background: 'var(--card-bg)',
|
||||
color: 'var(--text)',
|
||||
borderRadius: 'var(--radius)',
|
||||
padding: '8px 10px',
|
||||
cursor: 'pointer',
|
||||
boxShadow: 'var(--shadow-sm)',
|
||||
}
|
||||
|
||||
const modalPrimaryButtonStyle: React.CSSProperties = {
|
||||
border: '1px solid var(--accent)',
|
||||
background: 'var(--accent-soft)',
|
||||
color: 'var(--text-strong)',
|
||||
borderRadius: 'var(--radius)',
|
||||
padding: '10px 14px',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 600,
|
||||
boxShadow: 'var(--shadow-sm)',
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={onClose}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
background: 'rgba(0, 0, 0, 0.5)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 1000,
|
||||
padding: '16px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
width: '100%',
|
||||
maxWidth: '500px',
|
||||
maxHeight: '80vh',
|
||||
background: 'var(--surface-panel)',
|
||||
color: 'var(--text)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--radius)',
|
||||
padding: '20px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '16px',
|
||||
boxShadow: 'var(--shadow-md)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: '12px',
|
||||
}}
|
||||
>
|
||||
<button onClick={onPreviousDay} style={modalIconButtonStyle}>
|
||||
←
|
||||
</button>
|
||||
|
||||
<h2 style={{ margin: 0, textAlign: 'center', flex: 1 }}>
|
||||
{selectedDate.toLocaleDateString('en-US', {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</h2>
|
||||
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<button onClick={onNextDay} style={modalIconButtonStyle}>
|
||||
→
|
||||
</button>
|
||||
<button onClick={onClose} style={modalIconButtonStyle}>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
overflowY: 'auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '10px',
|
||||
minHeight: '120px',
|
||||
padding: '20px',
|
||||
margin: '-6px',
|
||||
}}
|
||||
>
|
||||
{sortedEvents.length === 0 ? (
|
||||
<div
|
||||
style={{
|
||||
opacity: 0.75,
|
||||
padding: '8px 0',
|
||||
}}
|
||||
>
|
||||
No events scheduled
|
||||
</div>
|
||||
) : (
|
||||
sortedEvents.map((event) => (
|
||||
<button
|
||||
key={event.id}
|
||||
onClick={() => onEventClick?.(event)}
|
||||
style={{
|
||||
border: '1px solid var(--border)',
|
||||
background: 'var(--surface-item)',
|
||||
color: 'var(--text)',
|
||||
borderRadius: 'var(--radius)',
|
||||
padding: '14px 14px 14px 16px',
|
||||
textAlign: 'left',
|
||||
cursor: 'pointer',
|
||||
boxShadow: 'var(--shadow-sm)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '4px',
|
||||
transition:
|
||||
'transform 120ms ease, box-shadow 120ms ease, border-color 120ms ease',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '13px',
|
||||
color: 'var(--text-muted)',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{new Date(event.starts_at).toLocaleTimeString([], {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
color: 'var(--text-strong)',
|
||||
lineHeight: 1.25,
|
||||
}}
|
||||
>
|
||||
{event.title}
|
||||
</div>
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<button onClick={onCreateEvent} style={modalPrimaryButtonStyle}>
|
||||
New Event
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+40
-100
@@ -1,111 +1,51 @@
|
||||
:root {
|
||||
--text: #6b6375;
|
||||
--text-h: #08060d;
|
||||
--bg: #fff;
|
||||
--border: #e5e4e7;
|
||||
--code-bg: #f4f3ec;
|
||||
/* Radius */
|
||||
--radius: 10px;
|
||||
|
||||
/* Surface system */
|
||||
--surface-page: #f8f8f8;
|
||||
--surface-panel: #ffffff;
|
||||
--surface-item: #f2f2f2;
|
||||
--surface-hover: #eaeaea;
|
||||
|
||||
/* Text */
|
||||
--text: #444444;
|
||||
--text-strong: #111111;
|
||||
--text-muted: #888888;
|
||||
|
||||
/* Borders */
|
||||
--border: #dddddd;
|
||||
|
||||
/* Accent */
|
||||
--accent: #aa3bff;
|
||||
--accent-bg: rgba(170, 59, 255, 0.1);
|
||||
--accent-border: rgba(170, 59, 255, 0.5);
|
||||
--social-bg: rgba(244, 243, 236, 0.5);
|
||||
--shadow:
|
||||
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
|
||||
--accent-soft: rgba(170, 59, 255, 0.1);
|
||||
|
||||
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
|
||||
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
|
||||
--mono: ui-monospace, Consolas, monospace;
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 4px 10px rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 10px 25px rgba(0, 0, 0, 0.08);
|
||||
|
||||
font: 18px/145% var(--sans);
|
||||
letter-spacing: 0.18px;
|
||||
color-scheme: light dark;
|
||||
/* App base */
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
font-size: 16px;
|
||||
}
|
||||
background: var(--surface-page);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--text: #9ca3af;
|
||||
--text-h: #f3f4f6;
|
||||
--bg: #16171d;
|
||||
--border: #2e303a;
|
||||
--code-bg: #1f2028;
|
||||
--surface-page: #1a1a1a;
|
||||
--surface-panel: #252525ee;
|
||||
--surface-item: #383838bc;
|
||||
--surface-hover: #505050;
|
||||
|
||||
--text: #dddddd;
|
||||
--text-strong: #ffffff;
|
||||
--text-muted: #aaaaaa;
|
||||
|
||||
--border: #3a3a3a;
|
||||
|
||||
--accent: #c084fc;
|
||||
--accent-bg: rgba(192, 132, 252, 0.15);
|
||||
--accent-border: rgba(192, 132, 252, 0.5);
|
||||
--social-bg: rgba(47, 48, 58, 0.5);
|
||||
--shadow:
|
||||
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
|
||||
--accent-soft: rgba(192, 132, 252, 0.15);
|
||||
|
||||
--shadow-sm: 0 4px 10px rgba(0, 0, 0, 0.4);
|
||||
--shadow-md: 0 10px 30px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
#social .button-icon {
|
||||
filter: invert(1) brightness(2);
|
||||
}
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 1126px;
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
border-inline: 1px solid var(--border);
|
||||
min-height: 100svh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
font-family: var(--heading);
|
||||
font-weight: 500;
|
||||
color: var(--text-h);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 56px;
|
||||
letter-spacing: -1.68px;
|
||||
margin: 32px 0;
|
||||
@media (max-width: 1024px) {
|
||||
font-size: 36px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
}
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
line-height: 118%;
|
||||
letter-spacing: -0.24px;
|
||||
margin: 0 0 8px;
|
||||
@media (max-width: 1024px) {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
code,
|
||||
.counter {
|
||||
font-family: var(--mono);
|
||||
display: inline-flex;
|
||||
border-radius: 4px;
|
||||
color: var(--text-h);
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 15px;
|
||||
line-height: 135%;
|
||||
padding: 4px 8px;
|
||||
background: var(--code-bg);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user