@devdogsuga/newsletter 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,8 +5,9 @@ components with two callers:
5
5
 
6
6
  - **`apps/platform`** renders issues as pages at `/changelog` and
7
7
  `/changelog/<version>`, straight from `ChangelogEmail`.
8
- - **`@devdogsuga/devtools`** exports issues as files — `pnpm devtools
9
- newsletter` — an `.html` preview and an Outlook-importable `.eml` per issue.
8
+ - **`@devdogsuga/newsletter-cli`** exports issues as files — `pnpm
9
+ newsletter` from the Backstage root — an `.html` preview and an
10
+ Outlook-importable `.eml` per issue, and pushes or sends them.
10
11
 
11
12
  It deliberately does not go through `@devdogsuga/email`. That package's
12
13
  compiled-slot pipeline exists to prove a template cannot branch on its inputs,
@@ -102,9 +103,10 @@ file; the platform archive and the exported email can never disagree.
102
103
  ## Exporting
103
104
 
104
105
  ```bash
105
- pnpm --filter @devdogsuga/newsletter build # the CLI consumes dist/
106
- pnpm devtools newsletter # pick issues interactively
107
- pnpm devtools newsletter '*' --out ~/changelog
106
+ # rebuilds brand, events and newsletter first, so the export carries the
107
+ # checkout's current issues and club config
108
+ pnpm newsletter # pick issues interactively
109
+ pnpm newsletter '*' --out ~/changelog
108
110
  ```
109
111
 
110
112
  The `.eml` carries `X-Unsent: 1` and no `Message-ID`, so **classic Outlook
@@ -117,7 +119,7 @@ does. Either way the draft is a review copy: the send happens with `--send`
117
119
  ## Pushing a draft
118
120
 
119
121
  ```bash
120
- pnpm devtools newsletter 3.0.1 --push
122
+ pnpm newsletter 3.0.1 --push
121
123
  ```
122
124
 
123
125
  appends the issue — same MIME as the `.eml`, minus `X-Unsent` — straight into
@@ -136,12 +138,12 @@ differently, into Word HTML, with the same result.)
136
138
  ## Sending
137
139
 
138
140
  ```bash
139
- pnpm devtools newsletter 3.0.1 --send --to listserv@listserv.uga.edu
141
+ pnpm newsletter 3.0.1 --send listserv@listserv.uga.edu
140
142
  ```
141
143
 
142
144
  submits the issue over SMTP as the club mailbox, byte-for-byte as authored —
143
145
  no composer touches it, so recipients get the document with its stylesheet,
144
- pins and gradient underlays intact. `--to` is required and comma-separated;
146
+ pins and gradient underlays intact. `--send` takes the recipients, comma-separated;
145
147
  there is deliberately no interactive path to a send.
146
148
 
147
149
  The first `--push` or `--send` opens a browser; sign in as the mailbox and
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { EVENT_TZ, meetingLocation } from "@devdogsuga/brand/event";
2
3
  import { MARK_SIZES } from "./assets.js";
3
4
  import { bc, brc, darkModeCss, DOT_GRID_CLASS, paintCss, SLANTS_CLASS, tc, } from "./darkmode.js";
4
5
  import { blockShadow, chipColors, font, HEADING_TINTS, KIND, PALETTE, SITE, SOCIAL_LINKS, solidBg, FONTS_HREF, UGA, } from "./theme.js";
@@ -75,14 +76,50 @@ function CtaButton({ label, url, color, ground, fonts, compact = false, }) {
75
76
  msoLineHeightRule: "exactly",
76
77
  }, children: [label, " ", "\u25B8"] }) }) }) }) }));
77
78
  }
79
+ /** The chip accent for each kind, from `KIND` so `darkModeCss()` pins it. */
80
+ const KIND_COLOR = {
81
+ "Interest Meeting": KIND.interest,
82
+ "Build Session": KIND.build,
83
+ "Study Session": KIND.study,
84
+ Social: KIND.social,
85
+ "Demo Night": KIND.demo,
86
+ };
87
+ function inEventZone(at, options) {
88
+ return at.toLocaleString("en-US", { timeZone: EVENT_TZ, ...options });
89
+ }
90
+ /** `"6:00 – 7:30 PM"`, or `"11:00 AM – 1:00 PM"` across noon. */
91
+ function timeRange(startsAt, endsAt) {
92
+ const clock = (at) => inEventZone(at, { hour: "numeric", minute: "2-digit" }).split(" ");
93
+ const [start, startMeridiem] = clock(startsAt);
94
+ const [end, endMeridiem] = clock(endsAt);
95
+ return startMeridiem === endMeridiem
96
+ ? `${start} – ${end} ${endMeridiem}`
97
+ : `${start} ${startMeridiem} – ${end} ${endMeridiem}`;
98
+ }
99
+ /**
100
+ * What a card prints for a meeting, in Eastern time. A night with no kind is
101
+ * a workshop night, the same reading the site's calendar gives it.
102
+ */
103
+ function eventCard(meeting) {
104
+ const startsAt = new Date(meeting.startsAt);
105
+ return {
106
+ chip: meeting.kind ?? "Workshop",
107
+ color: (meeting.kind && KIND_COLOR[meeting.kind]) ?? KIND.workshop,
108
+ dow: inEventZone(startsAt, { weekday: "short" }).toUpperCase(),
109
+ month: inEventZone(startsAt, { month: "short" }),
110
+ day: inEventZone(startsAt, { day: "numeric" }),
111
+ time: timeRange(startsAt, new Date(meeting.endsAt)),
112
+ loc: meetingLocation(meeting.building, meeting.location) ?? "TBA",
113
+ };
114
+ }
78
115
  /**
79
116
  * An upcoming-event row, drawn the way the site's schedule list draws one: a
80
117
  * stacked weekday / day-number date column, a chip pill, a display-face
81
118
  * title, and the time + room in quiet sans.
82
119
  */
83
- function EventRow({ event, ctx, }) {
120
+ function EventRow({ meeting, ctx }) {
84
121
  const { fonts } = ctx;
85
- const [month, day] = event.date.split(" ");
122
+ const card = eventCard(meeting);
86
123
  return (_jsx("table", { ...TABLE_RESET, width: "100%", bgcolor: PALETTE.card, className: `${bc(PALETTE.card)} ${brc(PALETTE.border)}`, style: {
87
124
  border: `1px solid ${PALETTE.border}`,
88
125
  borderRadius: "10px",
@@ -94,29 +131,29 @@ function EventRow({ event, ctx, }) {
94
131
  letterSpacing: "2px",
95
132
  color: PALETTE.mute,
96
133
  textTransform: "uppercase",
97
- }, children: event.dow }), _jsx("div", { className: tc(PALETTE.ink), style: {
134
+ }, children: card.dow }), _jsx("div", { className: tc(PALETTE.ink), style: {
98
135
  ...font(800, 28, 1.1, fonts.display),
99
136
  color: PALETTE.ink,
100
137
  fontVariantNumeric: "tabular-nums",
101
138
  padding: "3px 0 2px",
102
- }, children: day }), _jsx("div", { className: tc(PALETTE.dim), style: {
139
+ }, children: card.day }), _jsx("div", { className: tc(PALETTE.dim), style: {
103
140
  ...font(700, 10, 1, fonts.display),
104
141
  letterSpacing: "2px",
105
142
  color: PALETTE.dim,
106
143
  textTransform: "uppercase",
107
- }, children: month })] }), _jsx(DividerCell, { vertical: true }), _jsx("td", { valign: "middle", style: { padding: "15px 17px" }, children: _jsx("table", { ...TABLE_RESET, width: "100%", children: _jsx("tbody", { children: _jsxs("tr", { children: [_jsxs("td", { valign: "middle", children: [_jsx(Chip, { label: event.chip, color: event.color, fonts: fonts }), _jsx("div", { className: tc(PALETTE.ink), style: {
144
+ }, children: card.month })] }), _jsx(DividerCell, { vertical: true }), _jsx("td", { valign: "middle", style: { padding: "15px 17px" }, children: _jsx("table", { ...TABLE_RESET, width: "100%", children: _jsx("tbody", { children: _jsxs("tr", { children: [_jsxs("td", { valign: "middle", children: [_jsx(Chip, { label: card.chip, color: card.color, fonts: fonts }), _jsx("div", { className: tc(PALETTE.ink), style: {
108
145
  ...font(800, 18, 1.25, fonts.display),
109
146
  color: PALETTE.ink,
110
147
  letterSpacing: "-0.2px",
111
148
  padding: "9px 0 5px",
112
- }, children: event.title }), _jsxs("div", { className: tc(PALETTE.mute), style: {
149
+ }, children: meeting.title }), _jsxs("div", { className: tc(PALETTE.mute), style: {
113
150
  ...font(400, 13, 1.5, fonts.sans),
114
151
  color: PALETTE.mute,
115
- }, children: [event.time, " ", _jsx("span", { className: tc(PALETTE.dim), style: { color: PALETTE.dim }, children: "\u00B7" }), " ", event.loc] }), event.blurb ? (_jsx("div", { className: tc(PALETTE.dim), style: {
152
+ }, children: [card.time, " ", _jsx("span", { className: tc(PALETTE.dim), style: { color: PALETTE.dim }, children: "\u00B7" }), " ", card.loc] }), _jsx("div", { className: tc(PALETTE.dim), style: {
116
153
  ...font(400, 13, 1.55, fonts.sans),
117
154
  color: PALETTE.dim,
118
155
  paddingTop: "7px",
119
- }, children: event.blurb })) : null] }), event.rsvp ? (_jsx("td", { valign: "middle", align: "right", width: 74, style: { paddingLeft: "10px" }, children: _jsx(CtaButton, { label: "RSVP", url: event.rsvp, color: event.color, ground: PALETTE.card, fonts: fonts, compact: true }) })) : null] }) }) }) })] }) }) }));
156
+ }, children: meeting.summary })] }), meeting.rsvpUrl ? (_jsx("td", { valign: "middle", align: "right", width: 74, style: { paddingLeft: "10px" }, children: _jsx(CtaButton, { label: "RSVP", url: meeting.rsvpUrl, color: card.color, ground: PALETTE.card, fonts: fonts, compact: true }) })) : null] }) }) }) })] }) }) }));
120
157
  }
121
158
  /** A `## heading` in the terminal voice. */
122
159
  function SectionHeading({ children, color, fonts, paddingBottom = 14, }) {
@@ -140,6 +177,7 @@ function QuietCard({ children }) {
140
177
  export function ChangelogEmail({ issue, ctx, }) {
141
178
  const { fonts, assets } = ctx;
142
179
  const featured = issue.featured;
180
+ const featuredCard = eventCard(featured);
143
181
  const arrowLinkStyle = {
144
182
  color: PALETTE.ink,
145
183
  textDecoration: "none",
@@ -187,11 +225,11 @@ export function ChangelogEmail({ issue, ctx, }) {
187
225
  ...font(400, 15, 1.6, fonts.sans),
188
226
  color: PALETTE.mute,
189
227
  paddingTop: "11px",
190
- }, children: issue.intro })] }) }), _jsx("tr", { children: _jsxs("td", { style: { padding: "20px 22px 6px" }, children: [_jsx(SectionHeading, { color: featured.color, fonts: fonts, children: issue.featuredLabel }), _jsx("table", { ...TABLE_RESET, width: "100%", bgcolor: PALETTE.card2, className: `${bc(PALETTE.card2)} ${brc(featured.color)}`, style: {
191
- border: `1px solid ${featured.color}`,
228
+ }, children: issue.intro })] }) }), _jsx("tr", { children: _jsxs("td", { style: { padding: "20px 22px 6px" }, children: [_jsx(SectionHeading, { color: featuredCard.color, fonts: fonts, children: issue.featuredLabel }), _jsx("table", { ...TABLE_RESET, width: "100%", bgcolor: PALETTE.card2, className: `${bc(PALETTE.card2)} ${brc(featuredCard.color)}`, style: {
229
+ border: `1px solid ${featuredCard.color}`,
192
230
  borderRadius: "10px",
193
- ...blockShadow(featured.color),
194
- }, children: _jsx("tbody", { children: _jsx("tr", { children: _jsxs("td", { style: { padding: "22px 22px 24px" }, children: [_jsx(Chip, { label: featured.chip, color: featured.color, ground: PALETTE.card2, fonts: fonts }), _jsx("div", { className: tc(PALETTE.ink), style: {
231
+ ...blockShadow(featuredCard.color),
232
+ }, children: _jsx("tbody", { children: _jsx("tr", { children: _jsxs("td", { style: { padding: "22px 22px 24px" }, children: [_jsx(Chip, { label: featuredCard.chip, color: featuredCard.color, ground: PALETTE.card2, fonts: fonts }), _jsx("div", { className: tc(PALETTE.ink), style: {
195
233
  ...font(700, 30, 1.1, fonts.display),
196
234
  color: PALETTE.ink,
197
235
  letterSpacing: "-0.4px",
@@ -200,23 +238,23 @@ export function ChangelogEmail({ issue, ctx, }) {
200
238
  ...font(700, 14, 1.5, fonts.sans),
201
239
  color: PALETTE.ink,
202
240
  paddingRight: "8px",
203
- }, children: [featured.dow, ", ", featured.date] }), _jsx(DividerCell, { vertical: true }), _jsx("td", { className: tc(PALETTE.mute), style: {
241
+ }, children: [featuredCard.dow, ", ", featuredCard.month, " ", featuredCard.day] }), _jsx(DividerCell, { vertical: true }), _jsx("td", { className: tc(PALETTE.mute), style: {
204
242
  ...font(400, 14, 1.5, fonts.sans),
205
243
  color: PALETTE.mute,
206
244
  padding: "0 8px",
207
- }, children: featured.time }), _jsx(DividerCell, { vertical: true }), _jsx("td", { className: tc(PALETTE.mute), style: {
245
+ }, children: featuredCard.time }), _jsx(DividerCell, { vertical: true }), _jsx("td", { className: tc(PALETTE.mute), style: {
208
246
  ...font(400, 14, 1.5, fonts.sans),
209
247
  color: PALETTE.mute,
210
248
  padding: "0 8px",
211
- }, children: featured.loc })] }) }) }), _jsx("div", { className: tc(PALETTE.mute), style: {
249
+ }, children: featuredCard.loc })] }) }) }), _jsx("div", { className: tc(PALETTE.mute), style: {
212
250
  ...font(400, 15, 1.6, fonts.sans),
213
251
  color: PALETTE.mute,
214
252
  paddingTop: "10px",
215
- }, children: featured.blurb }), _jsx("div", { style: {
253
+ }, children: featured.summary }), _jsx("div", { style: {
216
254
  height: "18px",
217
255
  lineHeight: "18px",
218
256
  fontSize: "0px",
219
- }, children: " " }), _jsx(CtaButton, { label: issue.cta, url: featured.rsvp ?? `${SITE}/events`, color: UGA, fonts: fonts })] }) }) }) })] }) }), _jsx("tr", { children: _jsxs("td", { style: { padding: "22px 22px 0" }, children: [_jsx(SectionHeading, { color: PALETTE.ink, fonts: fonts, children: "upcoming" }), issue.upcoming.map((event, index) => (_jsx(EventRow, { event: event, ctx: ctx }, `${event.date}-${index}`))), _jsx("div", { style: { padding: "2px 0 4px", textAlign: "center" }, children: _jsx("a", { href: `${SITE}/events`, className: tc(KIND.build), style: {
257
+ }, children: " " }), _jsx(CtaButton, { label: issue.cta, url: featured.rsvpUrl ?? `${SITE}/events`, color: UGA, fonts: fonts })] }) }) }) })] }) }), _jsx("tr", { children: _jsxs("td", { style: { padding: "22px 22px 0" }, children: [_jsx(SectionHeading, { color: PALETTE.ink, fonts: fonts, children: "upcoming" }), issue.upcoming.map((meeting) => (_jsx(EventRow, { meeting: meeting, ctx: ctx }, meeting.id))), _jsx("div", { style: { padding: "2px 0 4px", textAlign: "center" }, children: _jsx("a", { href: `${SITE}/events`, className: tc(KIND.build), style: {
220
258
  ...font(700, 13, 1, fonts.mono),
221
259
  color: KIND.build,
222
260
  textDecoration: "none",
package/dist/index.d.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  export { ChangelogDocument, ChangelogEmail } from "./components.js";
10
10
  export { paintCss } from "./darkmode.js";
11
11
  export { DATA_URI_ASSETS, DEVDOGS_LOCKUP, GDGC_UGA, MARK_SIZES, type NewsletterAssets, type RenderContext, } from "./assets.js";
12
- export { type ChangelogEvent, type ChangelogIssue, ISSUES, issueByVersion, } from "./issues.js";
12
+ export { type ChangelogIssue, ISSUES, issueByVersion } from "./issues.js";
13
13
  export { SOCIAL_ICON_NAMES, socialIconDataUri, socialIconSvg, type SocialIconName, } from "./icons.js";
14
14
  export { CSS_VARIABLE_FONTS, EMAIL_FONTS, FONTS_HREF, type FontStacks, KIND, PALETTE, SITE, SOCIAL_LINKS, UGA, } from "./theme.js";
15
15
  import { type RenderContext } from "./assets.js";
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@
9
9
  export { ChangelogDocument, ChangelogEmail } from "./components.js";
10
10
  export { paintCss } from "./darkmode.js";
11
11
  export { DATA_URI_ASSETS, DEVDOGS_LOCKUP, GDGC_UGA, MARK_SIZES, } from "./assets.js";
12
- export { ISSUES, issueByVersion, } from "./issues.js";
12
+ export { ISSUES, issueByVersion } from "./issues.js";
13
13
  export { SOCIAL_ICON_NAMES, socialIconDataUri, socialIconSvg, } from "./icons.js";
14
14
  export { CSS_VARIABLE_FONTS, EMAIL_FONTS, FONTS_HREF, KIND, PALETTE, SITE, SOCIAL_LINKS, UGA, } from "./theme.js";
15
15
  import { DATA_URI_ASSETS } from "./assets.js";
package/dist/issues.d.ts CHANGED
@@ -1,18 +1,13 @@
1
- export interface ChangelogEvent {
2
- /** The chip label — the calendar's event-type vocabulary. */
3
- chip: string;
4
- /** The event-type accent from `KIND`. */
5
- color: string;
6
- title: string;
7
- /** Three-letter weekday, uppercase. */
8
- dow: string;
9
- /** `"Sep 9"` — month then day, split for the stacked date column. */
10
- date: string;
11
- time: string;
12
- loc: string;
13
- rsvp: string | null;
14
- blurb: string;
15
- }
1
+ /**
2
+ * The Changelog's content: the dated sends themselves, listing events read
3
+ * from the club config in `@devdogsuga/events`.
4
+ *
5
+ * Issues are versioned with semver — a send is a release. Issue copy lives
6
+ * here rather than in the components so a copy pass before a send touches one
7
+ * file, and so the platform's archive pages and the exported emails can never
8
+ * disagree about what an issue said.
9
+ */
10
+ import { type Meeting } from "@devdogsuga/events";
16
11
  export interface ChangelogIssue {
17
12
  /** Semver, doubling as the URL segment and the export filename. */
18
13
  version: string;
@@ -29,10 +24,10 @@ export interface ChangelogIssue {
29
24
  intro: string;
30
25
  /** The `##` heading over the hero card, e.g. `"happening_today"`. */
31
26
  featuredLabel: string;
32
- featured: ChangelogEvent;
27
+ featured: Meeting;
33
28
  /** The hero card's button label. */
34
29
  cta: string;
35
- upcoming: ChangelogEvent[];
30
+ upcoming: Meeting[];
36
31
  signoff: string;
37
32
  }
38
33
  export declare const ISSUES: ChangelogIssue[];
package/dist/issues.js CHANGED
@@ -1,106 +1,34 @@
1
1
  /**
2
- * The Changelog's content: event data authored in `@devdogsuga/events`'s
3
- * meetings (times converted to ET) and the dated sends themselves.
2
+ * The Changelog's content: the dated sends themselves, listing events read
3
+ * from the club config in `@devdogsuga/events`.
4
4
  *
5
- * Issues are versioned with semver — a send is a release. Event copy lives
5
+ * Issues are versioned with semver — a send is a release. Issue copy lives
6
6
  * here rather than in the components so a copy pass before a send touches one
7
7
  * file, and so the platform's archive pages and the exported emails can never
8
8
  * disagree about what an issue said.
9
9
  */
10
- import { KIND } from "./theme.js";
11
- const DEV_SESSION_BLURB = "Catch up on workshop materials, meet your teammates for hackathons, get unblocked by focus leads and officers, or just come hang out and get work done.";
10
+ import { getClubConfig } from "@devdogsuga/events";
11
+ const MEETINGS = new Map(getClubConfig().meetings.map((m) => [m.id, m]));
12
+ /** A meeting from the club config, by id. Throws on a typo rather than
13
+ * rendering an issue with a hole in it. */
14
+ function meeting(id) {
15
+ const found = MEETINGS.get(id);
16
+ if (found === undefined) {
17
+ throw new Error(`No meeting "${id}" in the club config.`);
18
+ }
19
+ return found;
20
+ }
21
+ /** Every event an issue lists, by its id in the club config. */
12
22
  const EVENTS = {
13
- interest: {
14
- chip: "Interest Meeting",
15
- color: KIND.interest,
16
- title: "Interest Meeting",
17
- dow: "WED",
18
- date: "Sep 9",
19
- time: "6:00 – 7:00 PM",
20
- loc: "DLW 110",
21
- rsvp: "https://uga.campuslabs.com/engage/event/12664203",
22
- blurb: "Come meet the new leadership team and get the details on all things DevDogs. Returning? Learn what's changing, what's staying the same, and how to get involved this year. Free food, too.",
23
- },
24
- coldstart: {
25
- chip: "Kickoff",
26
- color: KIND.workshop,
27
- title: "Cold Start",
28
- dow: "MON",
29
- date: "Sep 14",
30
- time: "6:00 – 7:30 PM",
31
- loc: "DLW 124",
32
- rsvp: "https://uga.campuslabs.com/engage/event/12664196",
33
- blurb: "The inaugural meeting for the 2026–2027 year. Get set up to contribute to this year's projects. Plus, a collaborative coding workshop: an introduction to Git, GitHub, and how to contribute to a team project.",
34
- },
35
- dev1: {
36
- chip: "Dev Session",
37
- color: KIND.build,
38
- title: "Dev Session #1",
39
- dow: "WED",
40
- date: "Sep 16",
41
- time: "6:00 – 7:00 PM",
42
- loc: "DLW 124",
43
- rsvp: "https://uga.campuslabs.com/engage/event/12664183",
44
- blurb: DEV_SESSION_BLURB,
45
- },
46
- // The three workshop nights have no summary in events yet — these
47
- // blurbs are authored from their linked workshop topics; swap in the real
48
- // copy once it lands in the meetings config.
49
- nextflutter: {
50
- chip: "Workshop",
51
- color: KIND.workshop,
52
- title: "Workshops: Next.js & Flutter",
53
- dow: "MON",
54
- date: "Sep 21",
55
- time: "6:00 – 7:30 PM",
56
- loc: "DLW 124",
57
- rsvp: null,
58
- blurb: "A framework double-header: build for the web with Next.js and go cross-platform with Flutter.",
59
- },
60
- dev2: {
61
- chip: "Dev Session",
62
- color: KIND.build,
63
- title: "Dev Session #2",
64
- dow: "WED",
65
- date: "Sep 23",
66
- time: "6:00 – 7:00 PM",
67
- loc: "DLW 124",
68
- rsvp: "https://uga.campuslabs.com/engage/event/12664184",
69
- blurb: DEV_SESSION_BLURB,
70
- },
71
- supabase: {
72
- chip: "Workshop",
73
- color: KIND.workshop,
74
- title: "Workshop: Supabase",
75
- dow: "MON",
76
- date: "Sep 28",
77
- time: "6:00 – 7:30 PM",
78
- loc: "DLW 124",
79
- rsvp: null,
80
- blurb: "Get hands-on with Supabase: Postgres, auth, and realtime data for this year's projects.",
81
- },
82
- dev3: {
83
- chip: "Dev Session",
84
- color: KIND.build,
85
- title: "Dev Session #3",
86
- dow: "WED",
87
- date: "Sep 30",
88
- time: "6:00 – 7:00 PM",
89
- loc: "DLW 124",
90
- rsvp: "https://uga.campuslabs.com/engage/event/12664184",
91
- blurb: DEV_SESSION_BLURB,
92
- },
93
- career: {
94
- chip: "Workshop",
95
- color: KIND.workshop,
96
- title: "Workshop: Career Fair Readiness",
97
- dow: "MON",
98
- date: "Oct 5",
99
- time: "6:00 – 8:00 PM",
100
- loc: "DLW 110",
101
- rsvp: null,
102
- blurb: "Get ready for the fall career fair: resume polish, portfolio pointers, and how to talk about what you've built.",
103
- },
23
+ coldstart: meeting("rectaW4iGmfDA3uwQ"),
24
+ build1: meeting("rec1BrdXl7u8bYGXH"),
25
+ nextflutter: meeting("recBF3KxHMKsT4Mz8"),
26
+ build2: meeting("recGqvQqUDFlrXPRc"),
27
+ supabase: meeting("recqDUR1D3CQNBVe5"),
28
+ build3: meeting("rec6aLjA2ZhT45xuh"),
29
+ judging1: meeting("feature-competition-1-judging-2026"),
30
+ career: meeting("recljv0crLDtLIBPc"),
31
+ touchgrass1: meeting("touch-grass-1-2026"),
104
32
  };
105
33
  export const ISSUES = [
106
34
  {
@@ -116,11 +44,11 @@ export const ISSUES = [
116
44
  featured: EVENTS.coldstart,
117
45
  cta: "RSVP for Cold Start",
118
46
  upcoming: [
119
- EVENTS.dev1,
47
+ EVENTS.build1,
120
48
  EVENTS.nextflutter,
121
- EVENTS.dev2,
49
+ EVENTS.build2,
122
50
  EVENTS.supabase,
123
- EVENTS.dev3,
51
+ EVENTS.build3,
124
52
  EVENTS.career,
125
53
  ],
126
54
  signoff: "Doors tonight at 6 in DLW 124. Bring a laptop if you have one, and we'll get you set up to ship either way.",
@@ -137,9 +65,29 @@ export const ISSUES = [
137
65
  featuredLabel: "happening_tonight",
138
66
  featured: EVENTS.nextflutter,
139
67
  cta: "RSVP for Cold Start",
140
- upcoming: [EVENTS.dev2, EVENTS.supabase, EVENTS.dev3, EVENTS.career],
68
+ upcoming: [EVENTS.build2, EVENTS.supabase, EVENTS.build3, EVENTS.career],
141
69
  signoff: "Doors tonight at 6 in DLW 124. Bring a laptop if you have one, but we'll get you set up to ship either way.",
142
70
  },
71
+ {
72
+ version: "3.0.2",
73
+ term: "Fall 2026",
74
+ sendLabel: "Mon · Sep 28",
75
+ command: "changelog --date 2026-09-28",
76
+ title: "Give Your App a Backend Tonight (DevDogs Changelog v3.0.2)",
77
+ preview: "Tonight at 6: the Supabase workshop, then our first feature competition of the year kicks off.",
78
+ tagline: "Your app gets a backend tonight.",
79
+ intro: "Tonight's workshop adds Supabase to the app you started at the Next.js and Flutter workshops: sign-in, a Postgres database, and row-level security. Web and mobile tracks share one project. Then our first feature competition of the year kicks off: form a team of 2 to 4 and build a feature for the club's own platform. Entries close at 6 PM next Monday, when teams present their work.",
80
+ featuredLabel: "happening_tonight",
81
+ featured: EVENTS.supabase,
82
+ cta: "See the schedule",
83
+ upcoming: [
84
+ EVENTS.build3,
85
+ EVENTS.judging1,
86
+ EVENTS.career,
87
+ EVENTS.touchgrass1,
88
+ ],
89
+ signoff: "Doors tonight at 6 in DLW 124. Turn on GitHub two-factor before you come: you need it to join a team.",
90
+ },
143
91
  ];
144
92
  export function issueByVersion(version) {
145
93
  return ISSUES.find((issue) => issue.version === version);
package/dist/theme.d.ts CHANGED
@@ -21,6 +21,7 @@ export declare const KIND: {
21
21
  readonly build: "#00a6f4";
22
22
  readonly study: "#ffb900";
23
23
  readonly social: "#ff637e";
24
+ readonly demo: "#7c86ff";
24
25
  readonly workshop: "#00d492";
25
26
  };
26
27
  /** The mauve surfaces, matching the platform (mauve-950 canvas → -300 text). */
package/dist/theme.js CHANGED
@@ -9,9 +9,10 @@ export const UGA = "#ba0c2f";
9
9
  */
10
10
  export const KIND = {
11
11
  interest: EVENT_KIND_VISUALS["Interest Meeting"].accent,
12
- build: EVENT_KIND_VISUALS["Dev Session"].accent,
12
+ build: EVENT_KIND_VISUALS["Build Session"].accent,
13
13
  study: EVENT_KIND_VISUALS["Study Session"].accent,
14
14
  social: EVENT_KIND_VISUALS.Social.accent,
15
+ demo: EVENT_KIND_VISUALS["Demo Night"].accent,
15
16
  workshop: EVENT_SEGMENT_VISUALS.workshop.accent,
16
17
  };
17
18
  /** The mauve surfaces, matching the platform (mauve-950 canvas → -300 text). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devdogsuga/newsletter",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "The DevDogs Changelog: email-safe React components rendered as platform pages and exported as Outlook-importable .eml files",
@@ -30,7 +30,8 @@
30
30
  "dependencies": {
31
31
  "react": "^19.2.8",
32
32
  "react-dom": "^19.2.8",
33
- "@devdogsuga/brand": "0.1.1"
33
+ "@devdogsuga/brand": "0.1.2",
34
+ "@devdogsuga/events": "0.1.3"
34
35
  },
35
36
  "devDependencies": {
36
37
  "@types/node": "^26.2.0",