Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion examples/demo.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
{
"monthRange": {
"start": {
"year": 2025,
"month": 0
},
"end": {
"year": 2025,
"month": 11
}
},
"selectedYear": 2025,
"dateCells": {
"2024-11-31": {
Expand Down Expand Up @@ -559,5 +569,5 @@
"selectedColorTexture": "yellow",
"selectedView": "Linear",
"exportDate": "2025-08-27T09:41:27.424Z",
"version": "2.0"
"version": "3.0"
}
10 changes: 6 additions & 4 deletions src/components/Calendar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from "react"
import { useCalendar } from "../contexts/CalendarContext"
import CalendarTitle from "./CalendarTitle"
import CalendarSharingControls from "./CalendarSharingControls"
import ColorPicker from "./ColorPicker"
import SaveLoadData from "./SaveLoadData"
import ClassicView from "./views/ClassicView"
Expand All @@ -9,33 +10,34 @@ import LinearView from "./views/LinearView"
import ViewSelector from "./ViewSelector"

const Calendar: React.FC = () => {
const { selectedYear, dateCells, setDateCells, selectedColorTexture, selectedView, setSelectedView } = useCalendar()
const { monthRange, dateCells, setDateCells, selectedColorTexture, selectedView, setSelectedView } = useCalendar()

return (
<div style={{ padding: "20px", fontFamily: "Arial, sans-serif" }}>
<CalendarTitle />
<div className="no-print">
<CalendarSharingControls />
<ColorPicker />
<ViewSelector selectedView={selectedView} onViewChange={setSelectedView} />
</div>

{selectedView === "Linear" ? (
<LinearView
selectedYear={selectedYear}
monthRange={monthRange}
dateCells={dateCells}
setDateCells={setDateCells}
selectedColorTexture={selectedColorTexture}
/>
) : selectedView === "Classic" ? (
<ClassicView
selectedYear={selectedYear}
monthRange={monthRange}
dateCells={dateCells}
setDateCells={setDateCells}
selectedColorTexture={selectedColorTexture}
/>
) : (
<ColumnView
selectedYear={selectedYear}
monthRange={monthRange}
dateCells={dateCells}
setDateCells={setDateCells}
selectedColorTexture={selectedColorTexture}
Expand Down
118 changes: 118 additions & 0 deletions src/components/CalendarSharingControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import React, { useState } from "react"
import { useCalendar } from "../contexts/CalendarContext"
import { UI_COLORS } from "../utils/colors"
import { encodeCalendarPayload } from "../utils/shareEncoding"

const CalendarSharingControls: React.FC = () => {
const { currentCalendarId, availableCalendars, switchCalendar, getCurrentDataSnapshot, isInitialized } =
useCalendar()
const [copyState, setCopyState] = useState<"idle" | "copied" | "error">("idle")

if (!isInitialized || !currentCalendarId) {
return null
}

const handleShare = async () => {
const snapshot = getCurrentDataSnapshot()
if (!snapshot) return

const encoded = encodeCalendarPayload(snapshot)
const baseUrl = `${window.location.origin}${window.location.pathname}`
const shareUrl = `${baseUrl}?calendar=${currentCalendarId}&data=${encoded}`

try {
await navigator.clipboard.writeText(shareUrl)
setCopyState("copied")
window.setTimeout(() => setCopyState("idle"), 2500)
} catch (error) {
console.error("Failed to copy share link:", error)
setCopyState("error")
window.prompt("Copy this calendar link", shareUrl)
}
}

const handleCalendarChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
const nextId = event.target.value
if (nextId && nextId !== currentCalendarId) {
switchCalendar(nextId)
}
}

return (
<div
style={{
display: "flex",
flexWrap: "wrap",
alignItems: "center",
justifyContent: "center",
gap: "12px",
marginBottom: "20px",
}}
>
<label
style={{
display: "flex",
alignItems: "center",
gap: "8px",
color: UI_COLORS.text.secondary,
fontWeight: 600,
}}
>
<span>Calendar</span>
<select
value={currentCalendarId}
onChange={handleCalendarChange}
style={{
padding: "8px 12px",
borderRadius: "6px",
border: `1px solid ${UI_COLORS.border.tertiary}`,
backgroundColor: UI_COLORS.background.tertiary,
fontSize: "14px",
cursor: "pointer",
touchAction: "auto",
}}
>
{availableCalendars.map((calendar) => (
<option key={calendar.id} value={calendar.id}>
{calendar.label}
{calendar.external ? " • shared" : " • local"}
</option>
))}
</select>
</label>

<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<button
onClick={handleShare}
style={{
padding: "10px 18px",
fontSize: "14px",
fontWeight: "bold",
backgroundColor: UI_COLORS.button.primary.normal,
color: UI_COLORS.text.white,
border: "none",
borderRadius: "6px",
cursor: "pointer",
transition: "background-color 0.2s ease",
touchAction: "auto",
}}
onMouseEnter={(event) => {
event.currentTarget.style.backgroundColor = UI_COLORS.button.primary.hover
}}
onMouseLeave={(event) => {
event.currentTarget.style.backgroundColor = UI_COLORS.button.primary.normal
}}
>
Share calendar
</button>
{copyState === "copied" ? (
<span style={{ color: UI_COLORS.text.secondary, fontSize: "13px" }}>Link copied!</span>
) : copyState === "error" ? (
<span style={{ color: UI_COLORS.button.danger.normal, fontSize: "13px" }}>Copy failed, link shown in prompt.</span>
) : null}
</div>
</div>
)
}

export default CalendarSharingControls
108 changes: 100 additions & 8 deletions src/components/CalendarTitle.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,110 @@
import React from "react"
import { useCalendar } from "../contexts/CalendarContext"
import { isAfter, monthPointerToValue, MonthPointer, parseMonthPointerValue } from "../utils/monthRange"

const MONTH_NAMES = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
]

const CalendarTitle: React.FC = () => {
const { selectedYear, setSelectedYear } = useCalendar()
const { monthRange, setMonthRange } = useCalendar()
const currentYear = new Date().getFullYear()

const yearOptions = Array.from({ length: 7 }, (_, i) => currentYear - 1 + i)
const monthNames = MONTH_NAMES

const minYear = Math.min(currentYear - 3, monthRange.start.year, monthRange.end.year)
const maxYear = Math.max(currentYear + 8, monthRange.start.year, monthRange.end.year)

const monthOptions: { value: string; label: string }[] = []
for (let year = minYear; year <= maxYear; year++) {
for (let month = 0; month < 12; month++) {
const pointer: MonthPointer = { year, month }
monthOptions.push({
value: monthPointerToValue(pointer),
label: `${monthNames[month]} ${year}`,
})
}
}

const yearShortcutOptions: { value: string; label: string; key?: string }[] = []
for (let year = 2025; year <= 2030; year++) {
const pointer: MonthPointer = { year, month: 0 }
yearShortcutOptions.push({
value: monthPointerToValue(pointer),
label: `${year}`,
key: `year-${year}`,
})
}

const startOptions: { value: string; label: string; key?: string }[] = [
...yearShortcutOptions,
...monthOptions,
]

const handleStartChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
const newStart = parseMonthPointerValue(event.target.value)
let newEnd = monthRange.end

if (newStart.month === 0) {
newEnd = { year: newStart.year, month: 11 }
} else if (isAfter(newStart, newEnd)) {
newEnd = { ...newStart }
}

setMonthRange({ start: newStart, end: newEnd })
}

const handleEndChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
const newEnd = parseMonthPointerValue(event.target.value)
let newStart = monthRange.start

if (isAfter(newStart, newEnd)) {
newStart = { ...newEnd }
}

setMonthRange({ start: newStart, end: newEnd })
}

const headerPrefix = "My plans from"

return (
<h1 style={{ textAlign: "center", marginBottom: "30px" }}>
My plans for{" "}
{headerPrefix}{" "}
<select
value={monthPointerToValue(monthRange.start)}
onChange={handleStartChange}
style={{
fontSize: "inherit",
fontWeight: "inherit",
border: "none",
background: "transparent",
cursor: "pointer",
padding: "0",
borderRadius: "4px",
touchAction: "auto",
}}
>
{startOptions.map((option) => (
<option key={option.key ?? option.value} value={option.value}>
{option.label}
</option>
))}
</select>
{" to "}
<select
value={selectedYear}
onChange={(e) => setSelectedYear(parseInt(e.target.value))}
value={monthPointerToValue(monthRange.end)}
onChange={handleEndChange}
style={{
fontSize: "inherit",
fontWeight: "inherit",
Expand All @@ -24,9 +116,9 @@ const CalendarTitle: React.FC = () => {
touchAction: "auto",
}}
>
{yearOptions.map((yearOption) => (
<option key={yearOption} value={yearOption}>
{yearOption}
{monthOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
Expand Down
Loading