keystone-design-bootstrap 1.0.25 → 1.0.26

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keystone-design-bootstrap",
3
- "version": "1.0.25",
3
+ "version": "1.0.26",
4
4
  "description": "Keystone Design Bootstrap - Sections, Elements, and Theme System for customer websites",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -73,7 +73,7 @@ function renderField(
73
73
  const value = phoneValues[name] ?? '';
74
74
  const placeholder = nationalMask ? nationalMask.replace(/#/g, '0') : (field.placeholder ?? '');
75
75
 
76
- const handlePhoneChange = (value: string) => {
76
+ const handlePhoneChange: (value: string) => void = (value) => {
77
77
  const digits = value.replace(/\D/g, '');
78
78
  const formatted = nationalMask ? formatDigitsToMask(digits, nationalMask) : digits;
79
79
  setPhoneValues((prev) => ({ ...prev, [name]: formatted }));
@@ -1,6 +1,16 @@
1
1
  import { PhotoWithFallback } from '../elements';
2
2
  import type { SocialPost } from '../../types/api/social-post';
3
3
 
4
+ /** Get display image URLs from post: image_urls (API) or photo_attachments (ordered) */
5
+ function getPostImageUrls(post: SocialPost): string[] {
6
+ if (post.image_urls?.length) return post.image_urls;
7
+ const attachments = post.photo_attachments || [];
8
+ const sorted = [...attachments].sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0));
9
+ return sorted
10
+ .map((pa) => pa.photo?.large_url || pa.photo?.original_url || pa.photo?.medium_url || pa.photo?.thumbnail_url)
11
+ .filter((url): url is string => Boolean(url));
12
+ }
13
+
4
14
  interface SocialMediaGridProps {
5
15
  socialPosts?: SocialPost[] | null;
6
16
  title?: string;
@@ -55,7 +65,7 @@ export const SocialMediaGrid = ({
55
65
  {posts.length > 0 ? (
56
66
  <div className="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
57
67
  {posts.map((post) => {
58
- const images = post.media_urls?.images || [];
68
+ const images = getPostImageUrls(post);
59
69
  const firstImage = images[0] || '';
60
70
  const content = post.content_markdown?.replace(/[#*\[\]()]/g, '').trim() || '';
61
71
  const platform = post.platform || 'social';
@@ -65,6 +75,7 @@ export const SocialMediaGrid = ({
65
75
  <div key={post.id} className="flex flex-col bg-white overflow-hidden">
66
76
  <div className="w-full h-64 overflow-hidden">
67
77
  <PhotoWithFallback
78
+ item={post}
68
79
  photoUrl={firstImage}
69
80
  photoAlt={content.substring(0, 50) || 'Social post'}
70
81
  fallbackId={post.id}
@@ -93,7 +104,7 @@ export const SocialMediaGrid = ({
93
104
 
94
105
  {post.platform && (
95
106
  <a
96
- href={`#post-${post.id}`}
107
+ href={post.external_post_url || `https://${platform.toLowerCase()}.com`}
97
108
  target="_blank"
98
109
  rel="noopener noreferrer"
99
110
  className="font-body text-sm underline underline-offset-4 hover:no-underline mt-auto transition-colors"
@@ -1,6 +1,16 @@
1
1
  import { PhotoWithFallback } from '../elements';
2
2
  import type { SocialPost } from '../../types/api/social-post';
3
3
 
4
+ /** Get display image URLs from post: image_urls (API) or photo_attachments (ordered) */
5
+ function getPostImageUrls(post: SocialPost): string[] {
6
+ if (post.image_urls?.length) return post.image_urls;
7
+ const attachments = post.photo_attachments || [];
8
+ const sorted = [...attachments].sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0));
9
+ return sorted
10
+ .map((pa) => pa.photo?.large_url || pa.photo?.original_url || pa.photo?.medium_url || pa.photo?.thumbnail_url)
11
+ .filter((url): url is string => Boolean(url));
12
+ }
13
+
4
14
  interface SocialMediaGridProps {
5
15
  socialPosts?: SocialPost[] | null;
6
16
  title?: string;
@@ -63,7 +73,7 @@ export const SocialMediaGrid = ({
63
73
  {posts.length > 0 ? (
64
74
  <div className="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
65
75
  {posts.map((post) => {
66
- const images = post.media_urls?.images || [];
76
+ const images = getPostImageUrls(post);
67
77
  const firstImage = images[0] || '';
68
78
  const content = post.content_markdown?.replace(/[#*\[\]()]/g, '').trim() || '';
69
79
  const platform = post.platform || 'social';
@@ -73,6 +83,7 @@ export const SocialMediaGrid = ({
73
83
  <div key={post.id} className="flex flex-col bg-white rounded-2xl overflow-hidden shadow-sm hover:shadow-md transition-shadow duration-300">
74
84
  <div className="w-full h-64 overflow-hidden">
75
85
  <PhotoWithFallback
86
+ item={post}
76
87
  photoUrl={firstImage}
77
88
  photoAlt={content.substring(0, 50) || ''}
78
89
  fallbackId={post.id}
@@ -101,7 +112,7 @@ export const SocialMediaGrid = ({
101
112
 
102
113
  {post.platform && (
103
114
  <a
104
- href={`#post-${post.id}`}
115
+ href={post.external_post_url || `https://${platform.toLowerCase()}.com`}
105
116
  target="_blank"
106
117
  rel="noopener noreferrer"
107
118
  className="font-body text-sm underline underline-offset-4 hover:no-underline mt-auto transition-colors"
@@ -8,6 +8,16 @@ import { Button, PaginationPageMinimalCenter, PhotoWithFallback, RoundButton } f
8
8
  import { cx } from '../../utils/cx';
9
9
  import type { SocialPost } from '../../types/api/social-post';
10
10
 
11
+ /** Get display image URLs from post: image_urls (API) or photo_attachments (ordered) */
12
+ function getPostImageUrls(post: SocialPost): string[] {
13
+ if (post.image_urls?.length) return post.image_urls;
14
+ const attachments = post.photo_attachments || [];
15
+ const sorted = [...attachments].sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0));
16
+ return sorted
17
+ .map((pa) => pa.photo?.large_url || pa.photo?.original_url || pa.photo?.medium_url || pa.photo?.thumbnail_url)
18
+ .filter((url): url is string => Boolean(url));
19
+ }
20
+
11
21
  interface SocialMediaGridProps {
12
22
  socialPosts?: SocialPost[] | null;
13
23
  title?: string;
@@ -79,7 +89,7 @@ export const SocialMediaGrid = ({
79
89
  <>
80
90
  <ul className="grid grid-cols-1 gap-x-8 gap-y-10 sm:grid-cols-2 md:gap-y-12 lg:grid-cols-3 xl:grid-cols-4">
81
91
  {paginatedPosts.map((post, index) => {
82
- const images = post.media_urls?.images || [];
92
+ const images = getPostImageUrls(post);
83
93
  const hasMultipleImages = images.length > 1;
84
94
  const firstImage = images[0];
85
95
  const content = post.content_markdown?.replace(/[#*\[\]()]/g, '').trim() || '';
@@ -117,6 +127,7 @@ export const SocialMediaGrid = ({
117
127
  </Carousel.Root>
118
128
  ) : (
119
129
  <PhotoWithFallback
130
+ item={post}
120
131
  photoUrl={firstImage}
121
132
  photoAlt={`${post.platform} post`}
122
133
  fallbackId={`social-post-${post.id || index}`}
@@ -125,6 +136,7 @@ export const SocialMediaGrid = ({
125
136
  )
126
137
  ) : (
127
138
  <PhotoWithFallback
139
+ item={post}
128
140
  photoUrl={undefined}
129
141
  photoAlt={`${post.platform} post`}
130
142
  fallbackId={`social-post-${post.id || index}`}
@@ -154,7 +166,7 @@ export const SocialMediaGrid = ({
154
166
  )}
155
167
 
156
168
  <Button
157
- href={`https://${post.platform.toLowerCase()}.com`}
169
+ href={post.external_post_url || `https://${post.platform.toLowerCase()}.com`}
158
170
  target="_blank"
159
171
  rel="noopener noreferrer"
160
172
  color="link-color"
@@ -27,12 +27,12 @@ export type MetaPixelProps = {
27
27
  * provides a meta_pixel_id.
28
28
  */
29
29
  export function MetaPixel({ pixelId }: MetaPixelProps) {
30
- if (!pixelId || typeof pixelId !== 'string' || pixelId.trim() === '') {
30
+ const raw = typeof pixelId === 'string' ? pixelId.trim() : '';
31
+ const id = raw && raw !== 'null' && /^\d+$/.test(raw) ? raw : '';
32
+ if (!id) {
31
33
  return null;
32
34
  }
33
35
 
34
- const id = pixelId.trim();
35
-
36
36
  return (
37
37
  <>
38
38
  <Script
@@ -41,6 +41,7 @@ export function MetaPixel({ pixelId }: MetaPixelProps) {
41
41
  dangerouslySetInnerHTML={{ __html: PIXEL_SCRIPT(id) }}
42
42
  />
43
43
  <noscript>
44
+ {/* eslint-disable-next-line @next/next/no-img-element -- 1x1 tracking pixel for no-JS fallback; next/image not applicable in noscript */}
44
45
  <img
45
46
  height={1}
46
47
  width={1}
@@ -1,21 +1,31 @@
1
- // Social post type definitions
1
+ import { PhotoAttachment } from './photos';
2
+
3
+ // Social post type definitions (aligned with Rails SocialPostSerializer)
2
4
  export interface SocialPost {
3
5
  id: number;
4
6
  platform: string;
5
7
  content_markdown: string;
6
- media_urls: {
8
+ posted_at: string;
9
+ status?: string;
10
+ created_at: string;
11
+ updated_at: string;
12
+ /** Image URLs from photo_attachments (preferred for display) */
13
+ image_urls?: string[];
14
+ /** Photo attachments (same pattern as blog post, team member, etc.) */
15
+ photo_attachments?: PhotoAttachment[];
16
+ /** Legacy; prefer image_urls / photo_attachments */
17
+ media_urls?: {
7
18
  images?: string[];
8
19
  videos?: string[];
9
20
  };
10
- engagement_metrics: {
21
+ engagement_metrics?: {
11
22
  likes?: number;
12
23
  comments?: number;
13
24
  shares?: number;
14
25
  views?: number;
15
26
  };
16
- posted_at: string;
17
- created_at: string;
18
- updated_at: string;
27
+ /** Permalink to the post on the platform when available; only link when present to avoid broken links */
28
+ external_post_url?: string | null;
19
29
  }
20
30
 
21
31
  export interface SocialPostParams {