Skip to content

Commit 3a037c2

Browse files
committed
Update for US date format, and clickable images
1 parent 9fbad0a commit 3a037c2

2 files changed

Lines changed: 162 additions & 123 deletions

File tree

apps/dashboard/src/components/stack-companion.tsx

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,21 @@ import { FeatureRequestBoard } from './stack-companion/feature-request-board';
1515
import { UnifiedDocsWidget } from './stack-companion/unified-docs-widget';
1616

1717
/**
18-
* Compare two CalVer versions in YYYY.MM.DD format
18+
* Compare two US date versions in M/D/YY format
1919
* Returns true if version1 is newer than version2
2020
*/
21-
function isNewerCalVer(version1: string, version2: string): boolean {
22-
const parseCalVer = (version: string): Date | null => {
23-
const match = version.match(/^(\d{4})\.(\d{2})\.(\d{2})$/);
21+
function isNewerVersion(version1: string, version2: string): boolean {
22+
const parseUsDate = (version: string): Date | null => {
23+
const match = version.match(/^(\d{1,2})\/(\d{1,2})\/(\d{2})$/);
2424
if (!match) return null;
25-
const [, year, month, day] = match;
26-
return new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
25+
const [, month, day, year] = match;
26+
// Convert 2-digit year to 4-digit (assumes 2000s)
27+
const fullYear = 2000 + parseInt(year);
28+
return new Date(fullYear, parseInt(month) - 1, parseInt(day));
2729
};
2830

29-
const date1 = parseCalVer(version1);
30-
const date2 = parseCalVer(version2);
31+
const date1 = parseUsDate(version1);
32+
const date2 = parseUsDate(version2);
3133

3234
if (!date1 || !date2) {
3335
// Fallback to string comparison if parsing fails
@@ -193,7 +195,7 @@ export function StackCompanion({ className }: { className?: string }) {
193195
} else {
194196
const hasNewer = entries.some((entry: ChangelogEntry) => {
195197
if (entry.isUnreleased) return false;
196-
return isNewerCalVer(entry.version, lastSeen);
198+
return isNewerVersion(entry.version, lastSeen);
197199
});
198200
setHasNewVersions(hasNewer);
199201
}
@@ -231,7 +233,7 @@ export function StackCompanion({ className }: { className?: string }) {
231233
} else {
232234
const hasNewer = changelogData.some((entry: ChangelogEntry) => {
233235
if (entry.isUnreleased) return false;
234-
return isNewerCalVer(entry.version, lastSeen);
236+
return isNewerVersion(entry.version, lastSeen);
235237
});
236238
setHasNewVersions(hasNewer);
237239
}

apps/dashboard/src/components/stack-companion/changelog-widget.tsx

Lines changed: 150 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22

33
import { Button } from '@/components/ui';
44
import { getPublicEnvVar } from '@/lib/env';
5-
import { CalendarIcon, CaretDownIcon, CaretUpIcon, InfoIcon } from '@phosphor-icons/react';
5+
import { CalendarIcon, CaretDownIcon, CaretUpIcon, InfoIcon, XIcon } from '@phosphor-icons/react';
66
import { captureError } from '@stackframe/stack-shared/dist/utils/errors';
77
import { runAsynchronously } from '@stackframe/stack-shared/dist/utils/promises';
88
import Image from 'next/image';
9-
import { useCallback, useEffect, useRef, useState } from 'react';
9+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
10+
import { createPortal } from 'react-dom';
1011
import ReactMarkdown from 'react-markdown';
1112
import remarkGfm from 'remark-gfm';
1213

@@ -49,73 +50,73 @@ function markLatestVersionSeen(entries: ApiChangelogEntry[]) {
4950

5051

5152
const formatVersion = (version: string) => {
52-
// Convert YYYY.MM.DD to YY.MM.DD format for display
53-
const calVerMatch = version.match(/^(\d{4})\.(\d{2})\.(\d{2})$/);
54-
if (calVerMatch) {
55-
const [, year, month, day] = calVerMatch;
56-
const shortYear = year.slice(-2); // Get last 2 digits
57-
return `${shortYear}.${month}.${day}`;
58-
}
53+
// Version is already in US date format (M/D/YY), return as-is
5954
return version;
6055
};
6156

62-
// Markdown component overrides for changelog rendering
63-
const markdownComponents = {
64-
h3: ({ children }: { children?: React.ReactNode }) => (
65-
<h3 className="text-sm font-semibold text-foreground mt-4 mb-2 first:mt-0">
66-
{children}
67-
</h3>
68-
),
69-
p: ({ children }: { children?: React.ReactNode }) => (
70-
<p className="text-muted-foreground leading-relaxed mb-2 last:mb-0">
71-
{children}
72-
</p>
73-
),
74-
ul: ({ children }: { children?: React.ReactNode }) => (
75-
<ul className="list-disc list-outside ml-4 space-y-1 text-muted-foreground">
76-
{children}
77-
</ul>
78-
),
79-
li: ({ children }: { children?: React.ReactNode }) => (
80-
<li className="leading-relaxed">
81-
{children}
82-
</li>
83-
),
84-
code: ({ children }: { children?: React.ReactNode }) => (
85-
<code className="px-1.5 py-0.5 rounded bg-muted text-foreground text-xs font-mono">
86-
{children}
87-
</code>
88-
),
89-
blockquote: ({ children }: { children?: React.ReactNode }) => (
90-
<div className="bg-blue-50 dark:bg-blue-950/30 border border-blue-200 dark:border-blue-800 p-3 my-3 rounded-md">
91-
<div className="flex items-start gap-2">
92-
<InfoIcon className="h-3.5 w-3.5 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" />
93-
<div className="text-sm text-blue-800 dark:text-blue-200 leading-relaxed [&>p]:mb-0">
94-
{children}
95-
</div>
96-
</div>
97-
</div>
98-
),
99-
img: ({ src, alt, title }: { src?: string, alt?: string, title?: string }) => {
100-
if (!src) return null;
101-
return (
102-
<Image
103-
src={src}
104-
alt={alt || ''}
105-
title={title}
106-
width={800}
107-
height={600}
108-
className="rounded-lg border border-border max-w-full h-auto my-4"
109-
/>
110-
);
111-
},
112-
};
113-
11457
export function ChangelogWidget({ isActive, initialData }: ChangelogWidgetProps) {
11558
const [changelog, setChangelog] = useState<ChangelogItem[]>([]);
11659
const [loading, setLoading] = useState(!initialData);
11760
const [error, setError] = useState<string | null>(null);
11861
const hasFetchedRef = useRef(false);
62+
const [previewImage, setPreviewImage] = useState<{ src: string, alt: string } | null>(null);
63+
64+
// Markdown component overrides for changelog rendering
65+
const markdownComponents = useMemo(() => ({
66+
h3: ({ children }: { children?: React.ReactNode }) => (
67+
<h3 className="text-sm font-semibold text-foreground mt-4 mb-2 first:mt-0">
68+
{children}
69+
</h3>
70+
),
71+
p: ({ children }: { children?: React.ReactNode }) => (
72+
<p className="text-muted-foreground leading-relaxed mb-2 last:mb-0">
73+
{children}
74+
</p>
75+
),
76+
ul: ({ children }: { children?: React.ReactNode }) => (
77+
<ul className="list-disc list-outside ml-4 space-y-1 text-muted-foreground">
78+
{children}
79+
</ul>
80+
),
81+
li: ({ children }: { children?: React.ReactNode }) => (
82+
<li className="leading-relaxed">
83+
{children}
84+
</li>
85+
),
86+
code: ({ children }: { children?: React.ReactNode }) => (
87+
<code className="px-1.5 py-0.5 rounded bg-muted text-foreground text-xs font-mono">
88+
{children}
89+
</code>
90+
),
91+
blockquote: ({ children }: { children?: React.ReactNode }) => (
92+
<div className="bg-blue-50 dark:bg-blue-950/30 border border-blue-200 dark:border-blue-800 p-3 my-3 rounded-md">
93+
<div className="flex items-start gap-2">
94+
<InfoIcon className="h-3.5 w-3.5 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" />
95+
<div className="text-sm text-blue-800 dark:text-blue-200 leading-relaxed [&>p]:mb-0">
96+
{children}
97+
</div>
98+
</div>
99+
</div>
100+
),
101+
img: ({ src, alt }: { src?: string, alt?: string }) => {
102+
if (!src) return null;
103+
return (
104+
<button
105+
type="button"
106+
onClick={() => setPreviewImage({ src, alt: alt || '' })}
107+
className="block w-full cursor-zoom-in focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 rounded-lg"
108+
>
109+
<Image
110+
src={src}
111+
alt={alt || ''}
112+
width={800}
113+
height={600}
114+
className="rounded-lg border border-border max-w-full h-auto my-4 transition-opacity hover:opacity-90"
115+
/>
116+
</button>
117+
);
118+
},
119+
}), []);
119120

120121
const fetchChangelog = useCallback(async (signal?: AbortSignal) => {
121122
try {
@@ -197,65 +198,101 @@ export function ChangelogWidget({ isActive, initialData }: ChangelogWidgetProps)
197198
}
198199

199200
return (
200-
<div className="space-y-4">
201-
<div className="bg-muted/30 rounded-lg p-4">
202-
<div className="flex items-center justify-between">
203-
<div>
204-
<h3 className="text-sm font-semibold">Stack Auth releases</h3>
205-
</div>
206-
</div>
207-
{error && (
208-
<p className="text-xs text-destructive mt-2">
209-
{error}
210-
</p>
211-
)}
212-
</div>
213-
201+
<>
214202
<div className="space-y-4">
215-
{changelog.length === 0 && !error && (
216-
<div className="bg-muted/30 rounded-lg p-4 text-center">
217-
<CalendarIcon className="h-8 w-8 mx-auto mb-2 text-muted-foreground" />
218-
<p className="text-xs text-muted-foreground font-medium">
219-
No changelog entries found
220-
</p>
203+
<div className="bg-muted/30 rounded-lg p-4">
204+
<div className="flex items-center justify-between">
205+
<div>
206+
<h3 className="text-sm font-semibold">Stack Auth releases</h3>
207+
</div>
221208
</div>
222-
)}
209+
{error && (
210+
<p className="text-xs text-destructive mt-2">
211+
{error}
212+
</p>
213+
)}
214+
</div>
223215

224-
{changelog.map((entry) => (
225-
<div key={entry.id} className="bg-card rounded-lg border border-border">
226-
<div className="px-4 py-4 flex items-center justify-between">
227-
<div className="flex items-center gap-4">
228-
<h4 className="text-base font-semibold">v{formatVersion(entry.version)}</h4>
229-
</div>
230-
<Button
231-
variant="ghost"
232-
size="sm"
233-
className="h-7 w-7 p-0"
234-
onClick={() => toggleExpanded(entry.id)}
235-
>
236-
{entry.expanded ? (
237-
<CaretUpIcon className="h-3 w-3" />
238-
) : (
239-
<CaretDownIcon className="h-3 w-3" />
240-
)}
241-
</Button>
216+
<div className="space-y-4">
217+
{changelog.length === 0 && !error && (
218+
<div className="bg-muted/30 rounded-lg p-4 text-center">
219+
<CalendarIcon className="h-8 w-8 mx-auto mb-2 text-muted-foreground" />
220+
<p className="text-xs text-muted-foreground font-medium">
221+
No changelog entries found
222+
</p>
242223
</div>
224+
)}
243225

244-
{entry.expanded && (
245-
<div className="px-4 pb-4">
246-
<div className="text-sm leading-relaxed space-y-3">
247-
<ReactMarkdown
248-
remarkPlugins={[remarkGfm]}
249-
components={markdownComponents}
250-
>
251-
{entry.markdown}
252-
</ReactMarkdown>
226+
{changelog.map((entry) => (
227+
<div key={entry.id} className="bg-card rounded-lg border border-border">
228+
<div className="px-4 py-4 flex items-center justify-between">
229+
<div className="flex items-center gap-4">
230+
<h4 className="text-base font-semibold">{formatVersion(entry.version)}</h4>
253231
</div>
232+
<Button
233+
variant="ghost"
234+
size="sm"
235+
className="h-7 w-7 p-0"
236+
onClick={() => toggleExpanded(entry.id)}
237+
>
238+
{entry.expanded ? (
239+
<CaretUpIcon className="h-3 w-3" />
240+
) : (
241+
<CaretDownIcon className="h-3 w-3" />
242+
)}
243+
</Button>
254244
</div>
255-
)}
256-
</div>
257-
))}
245+
246+
{entry.expanded && (
247+
<div className="px-4 pb-4">
248+
<div className="text-sm leading-relaxed space-y-3">
249+
<ReactMarkdown
250+
remarkPlugins={[remarkGfm]}
251+
components={markdownComponents}
252+
>
253+
{entry.markdown}
254+
</ReactMarkdown>
255+
</div>
256+
</div>
257+
)}
258+
</div>
259+
))}
260+
</div>
258261
</div>
259-
</div>
262+
263+
{/* Image preview lightbox - rendered via portal to escape container constraints */}
264+
{previewImage && createPortal(
265+
<div
266+
className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/80 backdrop-blur-sm"
267+
onClick={() => setPreviewImage(null)}
268+
onKeyDown={(e) => e.key === 'Escape' && setPreviewImage(null)}
269+
role="dialog"
270+
aria-modal="true"
271+
aria-label="Image preview"
272+
>
273+
<button
274+
type="button"
275+
onClick={() => setPreviewImage(null)}
276+
className="absolute top-4 right-4 p-2 rounded-full bg-black/50 text-white hover:bg-black/70 transition-colors"
277+
aria-label="Close preview"
278+
>
279+
<XIcon className="h-6 w-6" />
280+
</button>
281+
<div
282+
className="relative max-w-[90vw] max-h-[90vh]"
283+
onClick={(e) => e.stopPropagation()}
284+
>
285+
<Image
286+
src={previewImage.src}
287+
alt={previewImage.alt}
288+
width={1600}
289+
height={1200}
290+
className="rounded-lg max-w-full max-h-[90vh] w-auto h-auto object-contain"
291+
/>
292+
</div>
293+
</div>,
294+
document.body
295+
)}
296+
</>
260297
);
261298
}

0 commit comments

Comments
 (0)