Skip to content

Commit b571db4

Browse files
committed
extension wrap up work
1 parent ca698a5 commit b571db4

17 files changed

Lines changed: 1526 additions & 277 deletions

apps/extension/src/App.tsx

Lines changed: 107 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,60 @@ import FeedView from "./components/FeedView";
55
import BookmarkView from "./components/BookmarkView";
66
import SearchView from "./components/SearchView";
77
import OriginalEmailView from "./components/OriginalEmailView";
8+
import type { EventItem } from "./data/types";
9+
import { openExternalUrl } from "./utils/linkUtils";
810

911
type View = "feed" | "bookmarks" | "search" | "email";
1012

13+
export type PageContext = "gmail" | "gcal";
14+
1115
export interface AppProps {
1216
onClose?: () => void;
17+
pageContext?: PageContext;
18+
/** Called by BookmarkView when the user hovers a bookmark card on GCal. */
19+
onPreviewSlot?: (event: EventItem | null) => void;
1320
}
1421

15-
export default function App({ onClose }: AppProps) {
22+
const DASHBOARD_URL =
23+
(import.meta.env.VITE_DASHBOARD_URL as string | undefined) ??
24+
"https://cornellloop.com";
25+
26+
export default function App({ onClose, pageContext = "gmail", onPreviewSlot }: AppProps) {
1627
const [view, setView] = useState<View>("feed");
1728
const [activeTab, setActiveTab] = useState<"feed" | "bookmarks">("feed");
1829
const [searchQuery, setSearchQuery] = useState("");
1930

31+
// ── Bookmark state ─────────────────────────────────────────────────────
32+
const [bookmarkedIds, setBookmarkedIds] = useState<Set<string>>(new Set());
33+
34+
const toggleBookmark = (id: string) => {
35+
setBookmarkedIds((prev) => {
36+
const next = new Set(prev);
37+
if (next.has(id)) {
38+
next.delete(id);
39+
} else {
40+
next.add(id);
41+
}
42+
return next;
43+
});
44+
};
45+
46+
// ── Email view ─────────────────────────────────────────────────────────
47+
const [emailEvent, setEmailEvent] = useState<EventItem | null>(null);
48+
49+
const handleEmailView = (event: EventItem) => {
50+
setEmailEvent(event);
51+
setView("email");
52+
};
53+
54+
// ── Navigation ─────────────────────────────────────────────────────────
2055
const isSearchMode = view === "search" || view === "email";
2156

2257
const handleTabChange = (tab: string) => {
2358
const t = tab as "feed" | "bookmarks";
2459
setActiveTab(t);
2560
setView(t);
61+
setEmailEvent(null);
2662
};
2763

2864
const handleSearchFocus = () => setView("search");
@@ -36,9 +72,16 @@ export default function App({ onClose }: AppProps) {
3672

3773
const handleBack = () => {
3874
setSearchQuery("");
75+
setEmailEvent(null);
3976
setView(activeTab);
4077
};
4178

79+
/** Populate the search bar when the user clicks a popular search term. */
80+
const handleSearchSelect = (term: string) => {
81+
setSearchQuery(term);
82+
setView("search");
83+
};
84+
4285
return (
4386
<div
4487
className="flex h-full flex-col overflow-hidden rounded-[12px] bg-white"
@@ -59,19 +102,70 @@ export default function App({ onClose }: AppProps) {
59102
/>
60103
</div>
61104

62-
{/* ── Scrollable content (CTA scrolls with content, not sticky) ── */}
63-
<div className="min-h-0 flex-1 overflow-y-auto px-6 py-[21px]">
64-
{view === "feed" && <FeedView />}
65-
{view === "bookmarks" && <BookmarkView />}
66-
{view === "search" && <SearchView query={searchQuery} />}
67-
{view === "email" && <OriginalEmailView />}
68-
69-
<div className="pt-[21px]">
70-
<Button variant="primary" size="cta">
71-
See more in dashboard
72-
</Button>
105+
{/* ── Main content: search uses pinned footer CTA; other views scroll with CTA inline ── */}
106+
{view === "search" ? (
107+
<div className="flex min-h-0 flex-1 flex-col">
108+
<div className="min-h-0 flex-1 overflow-y-auto px-6 py-[21px]">
109+
<SearchView
110+
query={searchQuery}
111+
onSearchSelect={handleSearchSelect}
112+
bookmarkedIds={bookmarkedIds}
113+
onBookmark={toggleBookmark}
114+
onEmailView={handleEmailView}
115+
/>
116+
</div>
117+
{/* Pinned to bottom of panel while search content scrolls */}
118+
<div
119+
className={[
120+
"shrink-0 border-t border-[var(--color-border)] bg-[var(--color-surface)]",
121+
"px-6 py-4",
122+
"shadow-[0_-4px_16px_rgba(0,0,0,0.06)]",
123+
].join(" ")}
124+
>
125+
<Button
126+
variant="primary"
127+
size="cta"
128+
className="w-full"
129+
onClick={() => openExternalUrl(DASHBOARD_URL)}
130+
>
131+
See more in dashboard
132+
</Button>
133+
</div>
73134
</div>
74-
</div>
135+
) : (
136+
<div className="min-h-0 flex-1 overflow-y-auto px-6 py-[21px]">
137+
{view === "feed" && (
138+
<FeedView
139+
bookmarkedIds={bookmarkedIds}
140+
onBookmark={toggleBookmark}
141+
onEmailView={handleEmailView}
142+
/>
143+
)}
144+
145+
{view === "bookmarks" && (
146+
<BookmarkView
147+
bookmarkedIds={bookmarkedIds}
148+
onBookmark={toggleBookmark}
149+
onEmailView={handleEmailView}
150+
pageContext={pageContext}
151+
onPreviewSlot={onPreviewSlot}
152+
/>
153+
)}
154+
155+
{view === "email" && <OriginalEmailView event={emailEvent} />}
156+
157+
{/* CTA — opens dashboard in a new tab */}
158+
<div className="pt-[21px]">
159+
<Button
160+
variant="primary"
161+
size="cta"
162+
onClick={() => openExternalUrl(DASHBOARD_URL)}
163+
>
164+
See more in dashboard
165+
</Button>
166+
</div>
167+
</div>
168+
)}
75169
</div>
76170
);
77171
}

apps/extension/src/components/BookmarkCard.tsx

Lines changed: 55 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,16 @@
1313
* │ Subtitle line 2 │
1414
* │ [Tag] [Tag] │ ← tags (optional)
1515
* │ ───────────────────────────────── │
16-
* │ [ RSVP ] [ Add to Calendar ] │ ← actions (each conditional)
16+
* │ [ RSVP / Apply ] [ Add to Calendar ] │ ← actions (each conditional)
1717
* └───────────────────────────────────────┘
1818
*
19-
* Uses design system components: DateBadge, Tag, Button
20-
* Uses design system SVG: bookmark-filled.svg (always filled — this is the
21-
* bookmarks view so every card is already saved)
19+
* Subtitle variants (Figma annotations):
20+
* string[] → event with time + location: ['4:00–5:30 pm', 'Hollister Hall 312']
21+
* string → informative summary or edge-case "Click to see original email"
22+
* undefined → no subtitle shown
2223
*
23-
* Subtitle variants (from Figma annotations):
24-
* events → string[] e.g. ['4:00 pm – 5:30 pm', 'Hollister Hall 312']
25-
* informative → string e.g. 'For early career designers and developers'
26-
* edge case → string e.g. 'Click to see original email'
24+
* When `onSubtitleClick` is provided (edge-case event), clicking the subtitle
25+
* opens OriginalEmailView. The cursor changes to pointer to signal interactivity.
2726
*/
2827

2928
import type { ComponentPropsWithoutRef } from "react";
@@ -54,30 +53,42 @@ export interface BookmarkCardProps extends ComponentPropsWithoutRef<"div"> {
5453
thumbnailVariant?: ThumbnailVariant;
5554
/** Day-of-month for the "date" thumbnail (e.g. 24). */
5655
day?: number | string;
57-
/** Abbreviated month for the "date" thumbnail (e.g. "Mar"). */
56+
/** Abbreviated month for the "date" thumbnail (e.g. "Apr"). */
5857
month?: string;
5958
/** Event title — DM Sans SemiBold 14 px, Neutral/900. */
6059
title: string;
6160
/**
62-
* Subtitle shown below the title in the event row.
61+
* Subtitle shown below the title.
62+
* string[] → multiple lines (e.g. time + location)
6363
* string → single line (informative summary or edge-case message)
64-
* string[] → multiple lines, e.g. ['4:00 pm – 5:30 pm', 'Hollister Hall 312']
6564
*/
6665
subtitle?: string | string[];
66+
/**
67+
* When provided, clicking the subtitle triggers this handler.
68+
* Used for edge-case events where the subtitle reads "Click to see original email".
69+
*/
70+
onSubtitleClick?: () => void;
6771
/** Neutral/200 category tags shown beneath the event row. */
6872
tags?: string[];
6973
/**
70-
* When provided, an RSVP button is rendered.
71-
* Figma annotation: "appears only if there's an RSVP link".
74+
* Primary action button (RSVP / Apply / Register).
75+
* Figma annotation: "appears only if there's a primary link".
76+
* Prefer `links[]` → `getPrimaryLink()` to derive this from EventItem.
7277
*/
73-
onRsvp?: () => void;
78+
primaryAction?: { label: string; onClick: () => void };
7479
/**
7580
* When provided, an Add to Calendar button is rendered.
7681
* Figma annotation: "appears only when there's specific date and time".
7782
*/
7883
onAddToCalendar?: () => void;
7984
/** Called when the filled bookmark icon is pressed to remove the save. */
8085
onUnbookmark?: () => void;
86+
/**
87+
* Hover handlers wired to GCal grid highlight (passed from BookmarkView).
88+
* noop when not on a GCal page.
89+
*/
90+
onPreviewEnter?: () => void;
91+
onPreviewLeave?: () => void;
8192
}
8293

8394
// ── Component ──────────────────────────────────────────────────────────────
@@ -90,17 +101,21 @@ export function BookmarkCard({
90101
month,
91102
title,
92103
subtitle,
104+
onSubtitleClick,
93105
tags,
94-
onRsvp,
106+
primaryAction,
95107
onAddToCalendar,
96108
onUnbookmark,
109+
onPreviewEnter,
110+
onPreviewLeave,
97111
className,
98112
...rest
99113
}: BookmarkCardProps) {
100114
const subtitleLines: string[] =
101115
subtitle == null ? [] : Array.isArray(subtitle) ? subtitle : [subtitle];
102116

103-
const hasActions = onRsvp != null || onAddToCalendar != null;
117+
const hasActions = primaryAction != null || onAddToCalendar != null;
118+
const subtitleClickable = onSubtitleClick != null && subtitleLines.length > 0;
104119

105120
return (
106121
<div
@@ -114,6 +129,8 @@ export function BookmarkCard({
114129
]
115130
.filter(Boolean)
116131
.join(" ")}
132+
onMouseEnter={onPreviewEnter}
133+
onMouseLeave={onPreviewLeave}
117134
{...rest}
118135
>
119136
{/* ── Org header ── */}
@@ -155,28 +172,43 @@ export function BookmarkCard({
155172
{/* ── Middle: event row + tags ── */}
156173
<div className="flex w-full flex-col gap-[var(--space-2)]">
157174
{/* Event row — DateBadge + text + filled bookmark */}
158-
<div className="flex w-full items-center gap-[var(--space-3)] rounded-[var(--radius-input)] bg-[var(--color-surface)] p-[var(--space-1-5)]">
175+
<div className="flex w-full items-start gap-[var(--space-3)] rounded-[var(--radius-input)] bg-[var(--color-surface)] p-[var(--space-1-5)]">
159176
<DateBadge variant={thumbnailVariant} day={day} month={month} />
160177

161178
{/* Title + subtitle */}
162179
<div className="flex min-w-0 flex-1 flex-col gap-[3px]">
163180
<p
164181
className={
165182
BODY2_SEMIBOLD +
166-
" w-full truncate text-[var(--color-neutral-900)]"
183+
" w-full text-[var(--color-neutral-900)]"
167184
}
168185
style={{ fontVariationSettings: "'opsz' 14" }}
169186
>
170187
{title}
171188
</p>
172189

173190
{subtitleLines.length > 0 && (
174-
<div className="flex flex-col">
191+
<div
192+
className={[
193+
"flex flex-col gap-[1px]",
194+
subtitleClickable
195+
? "cursor-pointer hover:underline"
196+
: "",
197+
].join(" ")}
198+
onClick={subtitleClickable ? onSubtitleClick : undefined}
199+
role={subtitleClickable ? "button" : undefined}
200+
tabIndex={subtitleClickable ? 0 : undefined}
201+
onKeyDown={
202+
subtitleClickable
203+
? (e) => e.key === "Enter" && onSubtitleClick?.()
204+
: undefined
205+
}
206+
>
175207
{subtitleLines.map((line, i) => (
176208
<p
177209
key={i}
178210
className={
179-
BODY3 + " w-full truncate text-[var(--color-neutral-700)]"
211+
BODY3 + " w-full text-[var(--color-neutral-700)]"
180212
}
181213
style={{ fontVariationSettings: "'opsz' 14" }}
182214
>
@@ -217,14 +249,14 @@ export function BookmarkCard({
217249
{/* ── Action buttons ── */}
218250
{hasActions && (
219251
<div className="flex w-full gap-[var(--space-2)]">
220-
{onRsvp && (
252+
{primaryAction && (
221253
<Button
222254
variant="secondary"
223255
size="cta"
224256
className="flex-1"
225-
onClick={onRsvp}
257+
onClick={primaryAction.onClick}
226258
>
227-
RSVP
259+
{primaryAction.label}
228260
</Button>
229261
)}
230262
{onAddToCalendar && (

0 commit comments

Comments
 (0)