keystone-design-bootstrap 1.0.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 +179 -0
- package/package.json +59 -0
- package/src/contexts/ThemeContext.tsx +34 -0
- package/src/contexts/index.ts +1 -0
- package/src/design_system/elements/IconComponent.tsx +98 -0
- package/src/design_system/elements/avatar/avatar-label-group.tsx +30 -0
- package/src/design_system/elements/avatar/avatar-profile-photo.tsx +125 -0
- package/src/design_system/elements/avatar/avatar.tsx +131 -0
- package/src/design_system/elements/avatar/base-components/avatar-add-button.tsx +34 -0
- package/src/design_system/elements/avatar/base-components/avatar-company-icon.tsx +26 -0
- package/src/design_system/elements/avatar/base-components/avatar-online-indicator.tsx +31 -0
- package/src/design_system/elements/avatar/base-components/index.tsx +4 -0
- package/src/design_system/elements/avatar/base-components/verified-tick.tsx +34 -0
- package/src/design_system/elements/avatar/utils.ts +12 -0
- package/src/design_system/elements/badges/avatar.tsx +132 -0
- package/src/design_system/elements/badges/badge-groups.tsx +176 -0
- package/src/design_system/elements/badges/badge-types.ts +266 -0
- package/src/design_system/elements/badges/badges.tsx +430 -0
- package/src/design_system/elements/breadcrumb/Breadcrumb.tsx +33 -0
- package/src/design_system/elements/button-group/button-group.tsx +106 -0
- package/src/design_system/elements/buttons/app-store-buttons-outline.tsx +378 -0
- package/src/design_system/elements/buttons/app-store-buttons.tsx +567 -0
- package/src/design_system/elements/buttons/button-utility.tsx +116 -0
- package/src/design_system/elements/buttons/button.aman.tsx +174 -0
- package/src/design_system/elements/buttons/button.tsx +271 -0
- package/src/design_system/elements/buttons/close-button.tsx +42 -0
- package/src/design_system/elements/buttons/round-button.tsx +29 -0
- package/src/design_system/elements/buttons/social-button.tsx +148 -0
- package/src/design_system/elements/buttons/social-logos.tsx +115 -0
- package/src/design_system/elements/carousel/carousel-base.tsx +308 -0
- package/src/design_system/elements/carousel/carousel.tsx +308 -0
- package/src/design_system/elements/checkbox/checkbox.tsx +120 -0
- package/src/design_system/elements/date-picker/calendar.tsx +101 -0
- package/src/design_system/elements/date-picker/cell.tsx +106 -0
- package/src/design_system/elements/date-picker/date-input.tsx +32 -0
- package/src/design_system/elements/date-picker/date-picker.tsx +86 -0
- package/src/design_system/elements/date-picker/date-range-picker.tsx +163 -0
- package/src/design_system/elements/date-picker/range-calendar.tsx +161 -0
- package/src/design_system/elements/date-picker/range-preset.tsx +28 -0
- package/src/design_system/elements/featured-icon/featured-icon.tsx +154 -0
- package/src/design_system/elements/form/form.tsx +10 -0
- package/src/design_system/elements/form/hook-form.tsx +75 -0
- package/src/design_system/elements/hint-text/hint-text.tsx +33 -0
- package/src/design_system/elements/index.tsx +158 -0
- package/src/design_system/elements/input/hint-text.tsx +33 -0
- package/src/design_system/elements/input/input-group.tsx +133 -0
- package/src/design_system/elements/input/input.aman.tsx +172 -0
- package/src/design_system/elements/input/input.tsx +271 -0
- package/src/design_system/elements/input/label.tsx +50 -0
- package/src/design_system/elements/label/label.tsx +50 -0
- package/src/design_system/elements/loading-indicator/loading-indicator.tsx +123 -0
- package/src/design_system/elements/map/GoogleMap.tsx +286 -0
- package/src/design_system/elements/markdown-renderer/MarkdownRenderer.tsx +155 -0
- package/src/design_system/elements/modals/modal.tsx +41 -0
- package/src/design_system/elements/pagination/pagination-base.tsx +378 -0
- package/src/design_system/elements/pagination/pagination-dot.tsx +54 -0
- package/src/design_system/elements/pagination/pagination-line.tsx +50 -0
- package/src/design_system/elements/pagination/pagination.tsx +330 -0
- package/src/design_system/elements/photo-fallback/photo-fallback.tsx +143 -0
- package/src/design_system/elements/progress-indicators/progress-circles.tsx +176 -0
- package/src/design_system/elements/progress-indicators/progress-indicators.tsx +123 -0
- package/src/design_system/elements/progress-indicators/simple-circle.tsx +29 -0
- package/src/design_system/elements/radio-buttons/radio-buttons.tsx +129 -0
- package/src/design_system/elements/rating/rating-badge.tsx +144 -0
- package/src/design_system/elements/rating/rating-stars.tsx +77 -0
- package/src/design_system/elements/select/combobox.tsx +152 -0
- package/src/design_system/elements/select/multi-select.tsx +363 -0
- package/src/design_system/elements/select/popover.tsx +34 -0
- package/src/design_system/elements/select/select-item.tsx +97 -0
- package/src/design_system/elements/select/select-native.tsx +69 -0
- package/src/design_system/elements/select/select.aman.tsx +75 -0
- package/src/design_system/elements/select/select.tsx +146 -0
- package/src/design_system/elements/shared-assets/credit-card/credit-card.tsx +237 -0
- package/src/design_system/elements/shared-assets/credit-card/icons.tsx +75 -0
- package/src/design_system/elements/shared-assets/iphone-mockup.tsx +172 -0
- package/src/design_system/elements/shared-assets/section-divider.tsx +12 -0
- package/src/design_system/elements/slideout-menus/slideout-menu.tsx +122 -0
- package/src/design_system/elements/tabs/tabs.tsx +225 -0
- package/src/design_system/elements/tags/base-components/tag-checkbox.tsx +45 -0
- package/src/design_system/elements/tags/base-components/tag-close-x.tsx +34 -0
- package/src/design_system/elements/tags/tags.tsx +176 -0
- package/src/design_system/elements/textarea/textarea.aman.tsx +52 -0
- package/src/design_system/elements/textarea/textarea.tsx +111 -0
- package/src/design_system/elements/toggle/toggle.tsx +140 -0
- package/src/design_system/elements/tooltip/tooltip.tsx +109 -0
- package/src/design_system/hooks/use-breakpoint.ts +37 -0
- package/src/design_system/hooks/use-resize-observer.ts +68 -0
- package/src/design_system/logo/keystone-logo-minimal.tsx +93 -0
- package/src/design_system/logo/keystone-logo.tsx +22 -0
- package/src/design_system/sections/about-home.aman.tsx +85 -0
- package/src/design_system/sections/about-home.tsx +115 -0
- package/src/design_system/sections/blog-cards.tsx +848 -0
- package/src/design_system/sections/blog-gallery.aman.tsx +77 -0
- package/src/design_system/sections/blog-gallery.tsx +204 -0
- package/src/design_system/sections/blog-home.aman.tsx +84 -0
- package/src/design_system/sections/blog-home.tsx +153 -0
- package/src/design_system/sections/blog-post.aman.tsx +74 -0
- package/src/design_system/sections/blog-post.tsx +301 -0
- package/src/design_system/sections/blog-section.aman.tsx +101 -0
- package/src/design_system/sections/blog-section.tsx +179 -0
- package/src/design_system/sections/contact-home.tsx +25 -0
- package/src/design_system/sections/contact-section.aman.tsx +173 -0
- package/src/design_system/sections/contact-section.tsx +143 -0
- package/src/design_system/sections/faq-grid.aman.tsx +79 -0
- package/src/design_system/sections/faq-grid.tsx +102 -0
- package/src/design_system/sections/faq-home.aman.tsx +92 -0
- package/src/design_system/sections/faq-home.tsx +134 -0
- package/src/design_system/sections/feature-tab.tsx +43 -0
- package/src/design_system/sections/feature-text.tsx +284 -0
- package/src/design_system/sections/footer-home.aman.tsx +62 -0
- package/src/design_system/sections/footer-home.tsx +259 -0
- package/src/design_system/sections/generic-header-component.tsx +103 -0
- package/src/design_system/sections/header-navigation.aman.tsx +360 -0
- package/src/design_system/sections/header-navigation.tsx +334 -0
- package/src/design_system/sections/hero-faq.aman.tsx +38 -0
- package/src/design_system/sections/hero-faq.tsx +55 -0
- package/src/design_system/sections/hero-generic-text.aman.tsx +49 -0
- package/src/design_system/sections/hero-generic-text.tsx +51 -0
- package/src/design_system/sections/hero-home.aman.tsx +84 -0
- package/src/design_system/sections/hero-home.tsx +246 -0
- package/src/design_system/sections/hero-location-detail.aman.tsx +33 -0
- package/src/design_system/sections/hero-location-detail.tsx +72 -0
- package/src/design_system/sections/hero-service-detail.aman.tsx +53 -0
- package/src/design_system/sections/hero-service-detail.tsx +51 -0
- package/src/design_system/sections/hero-social-media.aman.tsx +42 -0
- package/src/design_system/sections/hero-social-media.tsx +35 -0
- package/src/design_system/sections/hero-testimonials.aman.tsx +38 -0
- package/src/design_system/sections/hero-testimonials.tsx +55 -0
- package/src/design_system/sections/home-hero-component.tsx +228 -0
- package/src/design_system/sections/index.tsx +131 -0
- package/src/design_system/sections/job-gallery.aman.tsx +91 -0
- package/src/design_system/sections/job-gallery.tsx +183 -0
- package/src/design_system/sections/location-details-section.aman.tsx +179 -0
- package/src/design_system/sections/location-details-section.tsx +196 -0
- package/src/design_system/sections/location-grid.aman.tsx +76 -0
- package/src/design_system/sections/location-grid.tsx +123 -0
- package/src/design_system/sections/services-grid.aman.tsx +85 -0
- package/src/design_system/sections/services-grid.tsx +104 -0
- package/src/design_system/sections/services-home.aman.tsx +78 -0
- package/src/design_system/sections/services-home.tsx +131 -0
- package/src/design_system/sections/social-media-grid.aman.tsx +132 -0
- package/src/design_system/sections/social-media-grid.tsx +189 -0
- package/src/design_system/sections/statistics-section.aman.tsx +79 -0
- package/src/design_system/sections/statistics-section.tsx +97 -0
- package/src/design_system/sections/team-grid.aman.tsx +85 -0
- package/src/design_system/sections/team-grid.tsx +88 -0
- package/src/design_system/sections/testimonials-home.aman.tsx +113 -0
- package/src/design_system/sections/testimonials-home.tsx +90 -0
- package/src/design_system/sections/values-section.aman.tsx +73 -0
- package/src/design_system/sections/values-section.tsx +128 -0
- package/src/design_system/utils/icon-mapping.tsx +28 -0
- package/src/index.ts +7 -0
- package/src/lib/component-registry.ts +53 -0
- package/src/lib/hooks/index.ts +8 -0
- package/src/lib/hooks/use-breakpoint.ts +37 -0
- package/src/lib/hooks/use-clipboard.ts +79 -0
- package/src/lib/hooks/use-resize-observer.ts +68 -0
- package/src/lib/server-api.ts +115 -0
- package/src/styles/style-overrides.aman.css +101 -0
- package/src/styles/theme.css +224 -0
- package/src/styles/typography.css +430 -0
- package/src/themes/index.ts +23 -0
- package/src/types/api/blog-post.ts +53 -0
- package/src/types/api/company-information.ts +44 -0
- package/src/types/api/contact.ts +63 -0
- package/src/types/api/faq.ts +37 -0
- package/src/types/api/job-posting.ts +34 -0
- package/src/types/api/location.ts +36 -0
- package/src/types/api/photos.ts +28 -0
- package/src/types/api/service.ts +37 -0
- package/src/types/api/social-post.ts +28 -0
- package/src/types/api/team-member.ts +29 -0
- package/src/types/api/testimonial.ts +29 -0
- package/src/types/api/website-photos.ts +22 -0
- package/src/types/config.ts +21 -0
- package/src/types/index.ts +21 -0
- package/src/utils/countries.tsx +1351 -0
- package/src/utils/cx.ts +25 -0
- package/src/utils/gradient-placeholder.ts +59 -0
- package/src/utils/is-react-component.ts +33 -0
- package/src/utils/markdown-toc.ts +54 -0
- package/src/utils/photo-helpers.ts +94 -0
package/src/utils/cx.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { extendTailwindMerge } from "tailwind-merge";
|
|
2
|
+
|
|
3
|
+
const twMerge = extendTailwindMerge({
|
|
4
|
+
extend: {
|
|
5
|
+
theme: {
|
|
6
|
+
text: ["display-xs", "display-sm", "display-md", "display-lg", "display-xl", "display-2xl"],
|
|
7
|
+
},
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* This function is a wrapper around the twMerge function.
|
|
13
|
+
* It is used to merge the classes inside style objects.
|
|
14
|
+
*/
|
|
15
|
+
export const cx = twMerge;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* This function does nothing besides helping us to be able to
|
|
19
|
+
* sort the classes inside style objects which is not supported
|
|
20
|
+
* by the Tailwind IntelliSense by default.
|
|
21
|
+
*/
|
|
22
|
+
export function sortCx<T extends Record<string, string | number | Record<string, string | number | Record<string, string | number>>>>(classes: T): T {
|
|
23
|
+
return classes;
|
|
24
|
+
}
|
|
25
|
+
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates a beautiful gradient SVG data URL using the full color palette
|
|
3
|
+
* Based on gradients used in corporate marketing site animations
|
|
4
|
+
* Each seed (post ID or index) will consistently return the same gradient
|
|
5
|
+
*/
|
|
6
|
+
export const getGradientUrl = (seed: string | number): string => {
|
|
7
|
+
// Beautiful gradient combinations using the full palette from corporate marketing site
|
|
8
|
+
// Colors match Tailwind's default palette (purple, pink, blue, green, orange, indigo)
|
|
9
|
+
const gradients = [
|
|
10
|
+
// Purple gradients (like from-purple-200 to-purple-300)
|
|
11
|
+
['rgb(233, 213, 255)', 'rgb(221, 214, 254)', 'rgb(196, 181, 253)'], // purple-200 to purple-300
|
|
12
|
+
['rgb(250, 232, 255)', 'rgb(251, 207, 232)', 'rgb(244, 114, 182)'], // pink-200 to pink-300 via pink-400
|
|
13
|
+
// Blue gradients (like from-blue-200 to-blue-300)
|
|
14
|
+
['rgb(191, 219, 254)', 'rgb(147, 197, 253)', 'rgb(96, 165, 250)'], // blue-200 to blue-300 to blue-400
|
|
15
|
+
// Green gradients (like from-green-200 to-green-300)
|
|
16
|
+
['rgb(187, 247, 208)', 'rgb(134, 239, 172)', 'rgb(74, 222, 128)'], // green-200 to green-300 to green-400
|
|
17
|
+
// Pink to Purple (like from-pink-400 to-purple-500)
|
|
18
|
+
['rgb(244, 114, 182)', 'rgb(217, 70, 239)', 'rgb(168, 85, 247)'], // pink-400 to purple-500
|
|
19
|
+
// Blue to Purple (like from-blue-500 to-purple-600)
|
|
20
|
+
['rgb(59, 130, 246)', 'rgb(147, 51, 234)', 'rgb(147, 51, 234)'], // blue-500 to purple-600
|
|
21
|
+
// Indigo to Purple (like from-indigo-500 to-purple-600)
|
|
22
|
+
['rgb(99, 102, 241)', 'rgb(139, 92, 246)', 'rgb(147, 51, 234)'], // indigo-500 to purple-600
|
|
23
|
+
// Orange gradients (like from-orange-200 to-orange-300)
|
|
24
|
+
['rgb(254, 215, 170)', 'rgb(253, 186, 116)', 'rgb(251, 146, 60)'], // orange-200 to orange-300 to orange-400
|
|
25
|
+
// Purple to Pink (like from-purple-400 to-pink-400)
|
|
26
|
+
['rgb(192, 132, 252)', 'rgb(232, 121, 249)', 'rgb(244, 114, 182)'], // purple-400 to pink-400
|
|
27
|
+
// Softer blue-green blend
|
|
28
|
+
['rgb(165, 243, 252)', 'rgb(103, 232, 249)', 'rgb(134, 239, 172)'], // cyan-300 to green-400
|
|
29
|
+
// Warm pink-orange
|
|
30
|
+
['rgb(251, 207, 232)', 'rgb(254, 202, 202)', 'rgb(253, 186, 116)'], // pink-200 to orange-300
|
|
31
|
+
// Cool indigo-blue
|
|
32
|
+
['rgb(199, 210, 254)', 'rgb(165, 180, 252)', 'rgb(129, 140, 248)'], // indigo-200 to indigo-400
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
// Calculate consistent index from seed
|
|
36
|
+
const index = typeof seed === 'string'
|
|
37
|
+
? seed.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % gradients.length
|
|
38
|
+
: Number(seed) % gradients.length;
|
|
39
|
+
|
|
40
|
+
const [color1, color2, color3] = gradients[index];
|
|
41
|
+
|
|
42
|
+
// Generate SVG with gradient (diagonal like bg-gradient-to-br)
|
|
43
|
+
const svg = `
|
|
44
|
+
<svg width="600" height="400" xmlns="http://www.w3.org/2000/svg">
|
|
45
|
+
<defs>
|
|
46
|
+
<linearGradient id="grad${index}" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
47
|
+
<stop offset="0%" style="stop-color:${color1};stop-opacity:1" />
|
|
48
|
+
<stop offset="50%" style="stop-color:${color2};stop-opacity:1" />
|
|
49
|
+
<stop offset="100%" style="stop-color:${color3};stop-opacity:1" />
|
|
50
|
+
</linearGradient>
|
|
51
|
+
</defs>
|
|
52
|
+
<rect width="600" height="400" fill="url(#grad${index})" />
|
|
53
|
+
</svg>
|
|
54
|
+
`.trim();
|
|
55
|
+
|
|
56
|
+
// Convert to base64 data URL
|
|
57
|
+
return `data:image/svg+xml;base64,${btoa(svg)}`;
|
|
58
|
+
};
|
|
59
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/* We cannot use type `unknown` instead of `any` here because it will break the type assertion `isReactComponent` function is providing. */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
|
+
import type React from "react";
|
|
4
|
+
|
|
5
|
+
type ReactComponent = React.FC<any> | React.ComponentClass<any, any>;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Checks if a given value is a function component.
|
|
9
|
+
*/
|
|
10
|
+
export const isFunctionComponent = (component: any): component is React.FC<any> => {
|
|
11
|
+
return typeof component === "function";
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Checks if a given value is a class component.
|
|
16
|
+
*/
|
|
17
|
+
export const isClassComponent = (component: any): component is React.ComponentClass<any, any> => {
|
|
18
|
+
return typeof component === "function" && component.prototype && (!!component.prototype.isReactComponent || !!component.prototype.render);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Checks if a given value is a forward ref component.
|
|
23
|
+
*/
|
|
24
|
+
export const isForwardRefComponent = (component: any): component is React.ForwardRefExoticComponent<any> => {
|
|
25
|
+
return typeof component === "object" && component !== null && component.$$typeof.toString() === "Symbol(react.forward_ref)";
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Checks if a given value is a valid React component.
|
|
30
|
+
*/
|
|
31
|
+
export const isReactComponent = (component: any): component is ReactComponent => {
|
|
32
|
+
return isFunctionComponent(component) || isForwardRefComponent(component) || isClassComponent(component);
|
|
33
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for extracting table of contents from markdown
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface TableOfContentsItem {
|
|
6
|
+
id: string;
|
|
7
|
+
title: string;
|
|
8
|
+
level: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generate a URL-friendly ID from a heading text
|
|
13
|
+
*/
|
|
14
|
+
function slugify(text: string): string {
|
|
15
|
+
return text
|
|
16
|
+
.toLowerCase()
|
|
17
|
+
.trim()
|
|
18
|
+
.replace(/[^\w\s-]/g, '') // Remove special characters
|
|
19
|
+
.replace(/[\s_-]+/g, '-') // Replace spaces and underscores with hyphens
|
|
20
|
+
.replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Extract headings from markdown content and generate table of contents
|
|
25
|
+
*/
|
|
26
|
+
export function extractTableOfContents(markdown: string): TableOfContentsItem[] {
|
|
27
|
+
if (!markdown) return [];
|
|
28
|
+
|
|
29
|
+
const toc: TableOfContentsItem[] = [];
|
|
30
|
+
const lines = markdown.split('\n');
|
|
31
|
+
|
|
32
|
+
for (const line of lines) {
|
|
33
|
+
// Match markdown headings (##, ###, etc.)
|
|
34
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
35
|
+
if (headingMatch) {
|
|
36
|
+
const level = headingMatch[1].length;
|
|
37
|
+
const title = headingMatch[2].trim();
|
|
38
|
+
|
|
39
|
+
// Only include h2 and h3 headings in TOC
|
|
40
|
+
if (level >= 2 && level <= 3) {
|
|
41
|
+
const id = slugify(title);
|
|
42
|
+
toc.push({
|
|
43
|
+
id,
|
|
44
|
+
title,
|
|
45
|
+
level,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return toc;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper functions for extracting photo URLs from photo associations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { WebsitePhotos } from '../types/api/website-photos';
|
|
6
|
+
|
|
7
|
+
export interface PhotoAttachment {
|
|
8
|
+
id: number;
|
|
9
|
+
photo: {
|
|
10
|
+
id: number;
|
|
11
|
+
title: string;
|
|
12
|
+
thumbnail_url?: string;
|
|
13
|
+
medium_url?: string;
|
|
14
|
+
large_url?: string;
|
|
15
|
+
original_url?: string;
|
|
16
|
+
};
|
|
17
|
+
featured: boolean;
|
|
18
|
+
sort_order: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get the best available photo URL from a photos array
|
|
23
|
+
* Priority: featured photo > first photo > fallback
|
|
24
|
+
*/
|
|
25
|
+
export function getPhotoUrl(
|
|
26
|
+
photos?: PhotoAttachment[],
|
|
27
|
+
fallbackId?: number | string
|
|
28
|
+
): string | null {
|
|
29
|
+
// Priority 1: Featured photo from photos array
|
|
30
|
+
if (photos && photos.length > 0) {
|
|
31
|
+
const featuredPhoto = photos.find(pa => pa.featured);
|
|
32
|
+
const photoToUse = featuredPhoto || photos[0];
|
|
33
|
+
const photo = photoToUse.photo;
|
|
34
|
+
|
|
35
|
+
if (photo) {
|
|
36
|
+
return photo.large_url || photo.medium_url || photo.thumbnail_url || photo.original_url || null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Fallback (gradient or null)
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get avatar URL for team members or authors
|
|
46
|
+
*/
|
|
47
|
+
export function getAvatarUrl(
|
|
48
|
+
photos?: PhotoAttachment[],
|
|
49
|
+
fallbackId?: number | string,
|
|
50
|
+
name?: string
|
|
51
|
+
): string {
|
|
52
|
+
const photoUrl = getPhotoUrl(photos, fallbackId);
|
|
53
|
+
|
|
54
|
+
if (photoUrl) {
|
|
55
|
+
return photoUrl;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Fallback to UI avatars if name is provided
|
|
59
|
+
if (name) {
|
|
60
|
+
return `https://ui-avatars.com/api/?name=${encodeURIComponent(name)}&background=random`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Final fallback
|
|
64
|
+
return `https://ui-avatars.com/api/?name=User&background=random`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get featured image URL for blog posts
|
|
69
|
+
*/
|
|
70
|
+
export function getFeaturedImageUrl(
|
|
71
|
+
photos?: PhotoAttachment[],
|
|
72
|
+
fallbackId?: number | string
|
|
73
|
+
): string | null {
|
|
74
|
+
return getPhotoUrl(photos, fallbackId);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get logo URL from website_photos API (which aggregates from account_photos)
|
|
79
|
+
*
|
|
80
|
+
* The website_photos API endpoint returns logos from account_photos with photo_type: 'logo',
|
|
81
|
+
* with industry fallback. This is the primary and only source for logos.
|
|
82
|
+
*
|
|
83
|
+
* Returns undefined if no logo is available (for PhotoWithFallback gradient fallback)
|
|
84
|
+
*/
|
|
85
|
+
export function getLogoUrl(
|
|
86
|
+
websitePhotos?: WebsitePhotos | null
|
|
87
|
+
): string | undefined {
|
|
88
|
+
if (websitePhotos?.logo?.url) {
|
|
89
|
+
return websitePhotos.logo.url;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
|