Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 | 1x 1x 1x 1x 1x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 1x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x | 'use client'
import {useState} from 'react'
import {googleCalendarUrl} from '@/lib/ics'
import Dropdown from '@/components/Dropdown'
type R = {
id: string
date: string
time: string | null
place: string | null
note: string | null
}
const SITE = typeof window === 'undefined' ? '' : window.location.origin
/** "Add to calendar" for one rehearsal: an .ics file or a Google link. */
export function AddToCalendar({
r,
band,
}: {
r: R
band: {name: string; timezone: string}
}) {
const google = googleCalendarUrl({
uid: r.id,
date: r.date,
time: r.time,
title: `${band.name} rehearsal`,
tz: band.timezone,
location: r.place,
description: [r.note, `Charts and setlists: ${SITE}/setlists`]
.filter(Boolean)
.join('\n'),
})
return (
<Dropdown
trigger="Add to calendar ▾"
align="left"
triggerClassName="flex min-h-9 items-center text-sm font-semibold text-sky"
panelClassName="w-60"
>
<a
href={`/api/rehearsals/${r.id}/ics`}
className="block rounded-lg px-3 py-2.5 no-underline hover:bg-line"
>
Apple Calendar / Outlook
<span className="block text-xs text-faint">Downloads an .ics file</span>
</a>
<a
href={google}
target="_blank"
rel="noreferrer"
className="block rounded-lg px-3 py-2.5 no-underline hover:bg-line"
>
Google Calendar ↗
</a>
</Dropdown>
)
}
/**
* A personal feed of every rehearsal and gig. Calendar apps check it on
* their own, so new and changed ones appear without anyone adding them.
*/
export function SubscribeCalendar() {
const [url, setUrl] = useState<string | null>(null)
const [open, setOpen] = useState(false)
const [copied, setCopied] = useState(false)
const [busy, setBusy] = useState(false)
async function show() {
setOpen(!open)
if (url || open) return
const r = await fetch('/api/account/calendar')
const {url: existing} = await r.json()
if (existing) setUrl(existing)
else await make()
}
async function make() {
setBusy(true)
const r = await fetch('/api/account/calendar', {method: 'POST'})
setUrl((await r.json()).url)
setCopied(false)
setBusy(false)
}
const webcal = url?.replace(/^https?:/, 'webcal:')
return (
<div className="mt-3 border-t border-line pt-3">
<button
type="button"
onClick={show}
aria-expanded={open}
className="min-h-9 text-sm font-semibold text-sky"
>
Subscribe to rehearsals and gigs {open ? '▴' : '▾'}
</button>
{open && (
<div className="mt-2 space-y-3 text-sm">
<p className="text-muted">
Rehearsals and gigs, in every band you’re in, then show up in your
calendar by themselves. This link is yours: don’t share it.
</p>
{url ? (
<>
<div className="flex flex-wrap gap-2">
<a
href={webcal}
className="inline-flex min-h-11 items-center rounded-lg bg-accent px-4 font-bold text-on-accent no-underline"
>
Apple Calendar / Outlook
</a>
<a
href={`https://calendar.google.com/calendar/r?cid=${encodeURIComponent(webcal ?? '')}`}
target="_blank"
rel="noreferrer"
className="inline-flex min-h-11 items-center rounded-lg border border-line-2 px-4 no-underline"
>
Google Calendar ↗
</a>
</div>
<div className="flex items-center gap-2">
<input
readOnly
value={url}
aria-label="Calendar link"
className="min-h-10 min-w-0 flex-1 rounded-lg border border-line-2 bg-ink px-2 font-mono text-xs text-muted"
onFocus={(e) => e.target.select()}
/>
<button
type="button"
onClick={async () => {
try {
await navigator.clipboard.writeText(url)
setCopied(true)
} catch {}
}}
className="min-h-10 rounded-lg border border-line-2 px-3"
>
{copied ? 'Copied ✓' : 'Copy'}
</button>
</div>
<button
type="button"
onClick={make}
disabled={busy}
className="min-h-9 text-xs text-faint underline"
>
Reset link (stops the old one working)
</button>
</>
) : (
<p className="text-faint">Making your link…</p>
)}
</div>
)}
</div>
)
}
|