Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,10 @@ export default defineConfig({
collapsed: true,
autogenerate: { directory: '/snowflake/integrations' },
},
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want to maintain the same page order in the navigation panel as in AWS docs?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes sense, although not critical. It gives our docs a consistent feel.

label: 'Sample Apps',
slug: 'snowflake/sample-apps',
},
{
label: 'Tutorials',
collapsed: true,
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
53 changes: 38 additions & 15 deletions src/components/ApplicationsShowcase.astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,62 @@
import { ApplicationsShowcase } from './applications/ApplicationsShowcase';
import { getImage } from 'astro:assets';

interface Props { docs?: 'aws' | 'snowflake' }
const { docs = 'aws' } = Astro.props as Props;

// Import data
import applicationsData from '../data/developerhub/applications.json';
import services from '../data/developerhub/services.json';
import integrations from '../data/developerhub/integrations.json';

const applications = applicationsData.applications;
const allApplications = applicationsData.applications;
const applications = allApplications.filter((app: any) => app.docs === docs);

const images = import.meta.glob<{ default: ImageMetadata }>(
'/src/assets/images/aws/sample-apps/*.{jpeg,jpg,png,gif}'
const awsImages = import.meta.glob<{ default: ImageMetadata }>(
'/src/assets/images/aws/sample-apps/*.{jpeg,jpg,png,gif,svg}'
);
const snowflakeImages = import.meta.glob<{ default: ImageMetadata }>(
'/src/assets/images/snowflake/sample-apps/*.{jpeg,jpg,png,gif,svg}'
);

const applicationsUpdated = await Promise.all(
applications.map(async (application) => {
applications.map(async (application: any) => {
const updatedApplication = { ...application };
const imagePath = `/src/assets/images/aws/sample-apps/${application.teaser}`;

if (images[imagePath]) {
const optimizedLeadImage = await getImage({
src: images[imagePath](),
format: 'png',
width: 800,
quality: 90,
});
updatedApplication.teaser = optimizedLeadImage.src;
if (docs === 'aws') {
const imagePath = `/src/assets/images/aws/sample-apps/${application.teaser}`;
if (awsImages[imagePath]) {
const optimizedLeadImage = await getImage({
src: awsImages[imagePath](),
format: 'png',
width: 800,
quality: 90,
});
updatedApplication.teaser = optimizedLeadImage.src;
}
} else if (docs === 'snowflake') {
const teaserName = String(application.teaser || '').split('/').pop();
const imagePath = teaserName ? `/src/assets/images/snowflake/sample-apps/${teaserName}` : '';
if (teaserName && snowflakeImages[imagePath]) {
const optimizedLeadImage = await getImage({
src: snowflakeImages[imagePath](),
format: 'png',
width: 800,
quality: 90,
});
updatedApplication.teaser = optimizedLeadImage.src;
} else if (teaserName) {
updatedApplication.teaser = `/images/snowflake/sample-apps/${teaserName}`;
}
}
return updatedApplication;
})
);
---

---
<ApplicationsShowcase
applications={applicationsUpdated}
services={services}
integrations={integrations}
docs={docs}
client:load
/>
139 changes: 105 additions & 34 deletions src/components/applications/ApplicationsShowcase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,40 @@ import React, { useState, useMemo } from 'react';
interface Application {
name: string;
description: string;
githubUrl: string;
githubUrl?: string;
docsUrl?: string;
teaser: string;
services: string[];
integrations: string[];
useCases: string[];
// Snowflake-only
features?: string[];
}

interface FilterState {
services: string[];
useCases: string[];
integrations: string[];
// Snowflake-only
features: string[];
}

interface ApplicationsShowcaseProps {
applications: Application[];
services: Record<string, string>;
integrations: Record<string, string>;
docs?: 'aws' | 'snowflake';
}

const ApplicationCard: React.FC<{
app: Application;
services: Record<string, string>;
integrations: Record<string, string>;
}> = ({ app, services, integrations }) => {
docs?: 'aws' | 'snowflake';
}> = ({ app, services, integrations, docs }) => {
return (
<a
href={app.githubUrl}
href={app.githubUrl || app.docsUrl || '#'}
target="_blank"
rel="noopener noreferrer"
className="app-card"
Expand All @@ -43,19 +50,31 @@ const ApplicationCard: React.FC<{

<div className="card-content">
<h3 className="card-title">{app.name}</h3>
<div className="service-icons">
{app.services.slice(0, 10).map((serviceCode) => (
<div key={serviceCode} className="service-icon" title={services[serviceCode] || serviceCode}>
<img
src={`/images/aws/${serviceCode}.svg`}
alt={services[serviceCode] || serviceCode}
/>
</div>
))}
{app.services.length > 10 && (
<div className="service-more">+{app.services.length - 10}</div>
)}
</div>
{docs === 'aws' && (
<div className="service-icons">
{app.services.slice(0, 10).map((serviceCode) => (
<div key={serviceCode} className="service-icon" title={services[serviceCode] || serviceCode}>
<img
src={`/images/aws/${serviceCode}.svg`}
alt={services[serviceCode] || serviceCode}
/>
</div>
))}
{app.services.length > 10 && (
<div className="service-more">+{app.services.length - 10}</div>
)}
</div>
)}
{docs === 'snowflake' && (app.features?.length ?? 0) > 0 && (
<div className="feature-pills">
{(app.features as string[]).slice(0, 10).map((feature) => (
<span key={feature} className="feature-pill">{feature}</span>
))}
{(app.features as string[]).length > 10 && (
<div className="service-more">+{(app.features as string[]).length - 10}</div>
)}
</div>
)}
<p className="card-description">{app.description}</p>

<div className="card-footer">
Expand All @@ -73,11 +92,13 @@ export const ApplicationsShowcase: React.FC<ApplicationsShowcaseProps> = ({
applications,
services,
integrations,
docs,
}) => {
const [filters, setFilters] = useState<FilterState>({
services: [],
useCases: [],
integrations: [],
features: [],
});

const [searchTerm, setSearchTerm] = useState('');
Expand All @@ -89,6 +110,13 @@ export const ApplicationsShowcase: React.FC<ApplicationsShowcaseProps> = ({
return Array.from(allServices).sort((a, b) => (services[a] || a).localeCompare(services[b] || b));
}, [applications, services]);

const uniqueFeatures = useMemo(() => {
const allFeatures = new Set(
applications.flatMap(app => (app.features ?? []))
);
return Array.from(allFeatures).sort();
}, [applications]);

const uniqueUseCases = useMemo(() => {
const allUseCases = new Set(applications.flatMap(app => app.useCases));
return Array.from(allUseCases).sort();
Expand All @@ -109,13 +137,19 @@ export const ApplicationsShowcase: React.FC<ApplicationsShowcaseProps> = ({
app.name.toLowerCase().includes(searchLower) ||
app.description.toLowerCase().includes(searchLower) ||
app.useCases.some(useCase => useCase.toLowerCase().includes(searchLower)) ||
app.services.some(service => (services[service] || service).toLowerCase().includes(searchLower)) ||
(docs === 'aws' && app.services.some(service => (services[service] || service).toLowerCase().includes(searchLower))) ||
(docs === 'snowflake' && (app.features ?? []).some(feature => feature.toLowerCase().includes(searchLower))) ||
app.integrations.some(integration => (integrations[integration] || integration).toLowerCase().includes(searchLower));
if (!matchesSearch) return false;
}

// Other filters
if (filters.services.length > 0 && !filters.services.some(service => app.services.includes(service))) return false;
if (docs === 'aws') {
if (filters.services.length > 0 && !filters.services.some(service => app.services.includes(service))) return false;
} else if (docs === 'snowflake') {
const appFeatures = app.features ?? [];
if (filters.features.length > 0 && !filters.features.some(feature => appFeatures.includes(feature))) return false;
}
if (filters.useCases.length > 0 && !filters.useCases.some(useCase => app.useCases.includes(useCase))) return false;
if (filters.integrations.length > 0 && !filters.integrations.some(integration => app.integrations.includes(integration))) return false;

Expand All @@ -126,7 +160,7 @@ export const ApplicationsShowcase: React.FC<ApplicationsShowcaseProps> = ({
return filtered.sort((a, b) => {
return a.name.localeCompare(b.name);
});
}, [applications, filters, searchTerm, sortBy, services, integrations]);
}, [applications, filters, searchTerm, sortBy, services, integrations, docs]);

const isSingleResult = filteredApplications.length === 1;
const gridStyle = useMemo<React.CSSProperties>(() => ({
Expand All @@ -148,13 +182,15 @@ export const ApplicationsShowcase: React.FC<ApplicationsShowcaseProps> = ({
services: [],
useCases: [],
integrations: [],
features: [],
});
setSearchTerm('');
};

const hasActiveFilters = filters.services.length > 0 ||
filters.useCases.length > 0 ||
filters.integrations.length > 0 ||
filters.features.length > 0 ||
searchTerm.length > 0;

return (
Expand Down Expand Up @@ -401,6 +437,22 @@ export const ApplicationsShowcase: React.FC<ApplicationsShowcaseProps> = ({
font-size: 0.875rem;
}

.feature-pills {
display: flex;
gap: 0.375rem;
flex-wrap: wrap;
margin: 0 0 0.75rem 0;
}

.feature-pill {
padding: 0.25rem 0.5rem;
background: var(--sl-color-bg);
border: 1px solid var(--sl-color-gray-6);
border-radius: 0.25rem;
font-size: 0.75rem;
color: var(--sl-color-gray-3);
}

.card-footer {
display: flex;
justify-content: flex-start;
Expand Down Expand Up @@ -557,21 +609,39 @@ export const ApplicationsShowcase: React.FC<ApplicationsShowcaseProps> = ({
)}
</div>

<select
value={filters.services[0] || ''}
onChange={(e) => setFilters(prev => ({
...prev,
services: e.target.value ? [e.target.value] : []
}))}
className="filter-select"
>
<option value="">Services</option>
{uniqueServices.map((service) => (
<option key={service} value={service}>
{services[service] || service}
</option>
))}
</select>
{docs === 'aws' ? (
<select
value={filters.services[0] || ''}
onChange={(e) => setFilters(prev => ({
...prev,
services: e.target.value ? [e.target.value] : []
}))}
className="filter-select"
>
<option value="">Services</option>
{uniqueServices.map((service) => (
<option key={service} value={service}>
{services[service] || service}
</option>
))}
</select>
) : (
<select
value={(filters.features?.[0] as string) || ''}
onChange={(e) => setFilters(prev => ({
...prev,
features: e.target.value ? [e.target.value] : []
}))}
className="filter-select"
>
<option value="">Features</option>
{uniqueFeatures.map((feature) => (
<option key={feature} value={feature}>
{feature}
</option>
))}
</select>
)}

<select
value={filters.useCases[0] || ''}
Expand Down Expand Up @@ -624,6 +694,7 @@ export const ApplicationsShowcase: React.FC<ApplicationsShowcaseProps> = ({
app={app}
services={services}
integrations={integrations}
docs={docs}
/>
))}

Expand Down
2 changes: 1 addition & 1 deletion src/content/docs/aws/sample-apps.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ sidebar:

import ApplicationsShowcase from "../../../components/ApplicationsShowcase.astro";

<ApplicationsShowcase />
<ApplicationsShowcase docs="aws" />
13 changes: 13 additions & 0 deletions src/content/docs/snowflake/sample-apps.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
title: Sample Apps
description: Sample Apps to help LocalStack for Snowflake users adopt real-world scenarios to rapidly and conveniently create, configure, and test applications locally.
template: doc
sidebar:
order: 4
---

import ApplicationsShowcase from "../../../components/ApplicationsShowcase.astro";

<ApplicationsShowcase docs="snowflake" />


Loading