aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/components/ScheduleTable.astro
blob: b636171c72573fd962776fba5a268b01ed1618b8 (plain)
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
---
// 行事予定の一覧表。データは src/data/schedule/*.json から読む。
// セルの中身は HTML(リンクを含む)としてそのまま出力する。
import meetings from '@/data/schedule/meetings.json';
import events from '@/data/schedule/events.json';
import { entryDates, mainStartMonth } from '@/lib/schedule-dates';

type Row = { date: string; title: string; place: string; note: string; source: string };
type TableData = { columns: string[]; rows: Row[] };

const {
  variant,
  rows,
  scope = 'all',
} = Astro.props as { variant: 'meeting' | 'event'; rows?: Row[]; scope?: 'all' | 'main' };
const base: TableData = (variant === 'meeting' ? meetings : events) as TableData;
// rows を渡された場合はその行だけを表示する(月別・年度別アーカイブ用)
let list = rows ?? base.rows;

// scope="main" のときは、前月以降に開催される予定だけを表示する。
// 前々月以前の終了分はアーカイブに移し、メインページが年々肥大化しないようにする。
if (scope === 'main') {
  const start = `${mainStartMonth()}-01`;
  list = list.filter((row) =>
    entryDates(variant, row.date, row.place).some((date) => date >= start),
  );
}

const data: TableData = { columns: base.columns, rows: list };

// 「令和9年1月28日(木)」のような長い日付で列が広がるため、「令和9年」の直後で改行する。
const dateHtml = (html: string) =>
  html.replace(/(令和[0-90-9]+年)(?!\s*<br)/, '$1<br />');
---

<table>
  <thead>
    <tr>{data.columns.map((column) => <th>{column}</th>)}</tr>
  </thead>
  <tbody>
    {
      data.rows.map((row) => (
        <tr>
          <td set:html={dateHtml(row.date)} />
          <td set:html={row.title} />
          <td set:html={row.place} />
          <td set:html={row.note} />
          <td set:html={row.source} />
        </tr>
      ))
    }
  </tbody>
</table>

<script>
  // 行ごとの共有ボタン(X・Instagram・Facebook)。
  // 行数が多いため、HTMLには書かず、このスクリプトで後から差し込む。
  // アイコンは CSS(mask-image)で描くので、ボタンには文字もSVGも入れない。
  if (!document.documentElement.dataset.scheduleShareInit) {
    document.documentElement.dataset.scheduleShareInit = '1';

    const KINDS: { kind: string; label: string }[] = [
      { kind: 'x', label: 'Xでシェア' },
      { kind: 'copy', label: '文面をコピー' },
    ];

    function attachButtons() {
      for (const row of document.querySelectorAll<HTMLTableRowElement>(
        '.schedule-table tbody tr',
      )) {
        const titleCell = row.children[1];
        if (!titleCell || titleCell.querySelector('.schedule-share')) continue;

        const wrap = document.createElement('span');
        wrap.className = 'schedule-share';
        for (const { kind, label } of KINDS) {
          const button = document.createElement('button');
          button.type = 'button';
          button.className = `schedule-share-btn is-${kind}`;
          button.dataset.share = kind;
          button.setAttribute('aria-label', label);
          button.title = label;
          wrap.appendChild(button);
        }
        titleCell.appendChild(wrap);
      }
    }

    const strip = (el: Element | null | undefined) =>
      (el?.textContent ?? '').replace(/\s+/g, ' ').trim();

    // 共有する文面(日時+タイトル+時間と場所+申込み等)
    function shareText(row: Element): string {
      const cells = row.children;
      const lines = [`${strip(cells[0])} ${strip(cells[1])}`];
      const place = strip(cells[2]);
      const note = strip(cells[3]);
      if (place) lines.push(place);
      if (note) lines.push(note.length > 120 ? `${note.slice(0, 120)}…` : note);
      return lines.join('\n');
    }

    function rowUrl(row: Element): string {
      const base = location.origin + location.pathname;
      return row.id ? `${base}#${row.id}` : base;
    }

    document.addEventListener('click', async (event) => {
      const target = event.target as HTMLElement | null;
      const button = target?.closest<HTMLElement>('.schedule-share-btn');
      if (!button) return;
      const row = button.closest('tr');
      if (!row) return;

      const text = shareText(row);
      const url = rowUrl(row);
      const kind = button.dataset.share;

      if (kind === 'x') {
        window.open(
          `https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}&url=${encodeURIComponent(url)}`,
          '_blank',
          'noopener',
        );
        return;
      }

      // コピー:Instagram・Facebookなど、文面を直接渡せないSNSにも貼れる
      try {
        await navigator.clipboard.writeText(`${text}\n${url}`);
        const original = button.title;
        button.classList.add('is-done');
        button.title = 'コピーしました';
        button.setAttribute('aria-label', 'コピーしました');
        window.setTimeout(() => {
          button.classList.remove('is-done');
          button.title = original;
          button.setAttribute('aria-label', original);
        }, 1500);
      } catch {
        window.alert('このブラウザではコピーできませんでした。文面を選択してコピーしてください。');
      }
    });

    attachButtons();
    // アーカイブの絞り込みなどで行が増えた場合にも備える
    document.addEventListener('click', () => setTimeout(attachButtons, 0));
  }
</script>