Community website for the AWS User Group Cloud Del Norte (formerly awsaerospace), serving the North Coast region.
🌐 Live: https://d2ly3jmh1f74xt.cloudfront.net 📅 Meetup: https://www.meetup.com/cloud-del-norte/
| Layer | Technology |
|---|---|
| Bundler | Vite 7 (multi-page app) |
| UI | React 19 + Cloudscape Design System 3.x |
| Language | TypeScript 5.9 |
| Tests | Vitest + @testing-library/react |
| Linter | ESLint 10 (flat config — eslint.config.js) |
| Build output | ./lib/ |
| Hosting | S3 + CloudFront |
npm install # install dependencies
npm run dev # dev server at localhost:8080
npm run build # tsc + vite build → ./lib/
npm run lint # eslint
npm test # vitest run
npm run test:watch # vitest (watch mode)
npm run test:ui # vitest --ui
npm run coverage # vitest run --coverageQuality gate before deploy: npm run lint && npm test && npm run build
This is a multi-page app (MPA) — each page is an independent Vite entry point. There is no React Router and no shared runtime bundle between pages.
Every page requires exactly three files:
src/pages/<name>/
index.html ← Vite HTML entry point
main.tsx ← mounts React root, imports global styles + tokens
app.tsx ← page component tree wrapped in Shell
| Page | Path |
|---|---|
| Home | src/pages/home/ |
| Meetings | src/pages/meetings/ |
| Create Meeting | src/pages/create-meeting/ |
| Learning / API | src/pages/learning/api/ |
| Maintenance Calendar | src/pages/maintenance-calendar/ |
| Theme Preview | src/pages/theme/ |
- Create
src/pages/<name>/withindex.html,main.tsx, andapp.tsx - Register the entry in
vite.config.ts→build.rollupOptions.input - Add a nav item in
src/components/navigation/index.tsx
Every page must implement all of the following:
- Shell wrapper —
app.tsxwraps content in<Shell>fromsrc/layouts/shell - Theme state —
useState<Theme>initialized viainitializeTheme(), passed to Shell - Locale state —
useState<Locale>initialized viainitializeLocale(), passed to Shell inside<LocaleProvider> - Deep imports — all Cloudscape components imported via deep paths (e.g.
@cloudscape-design/components/button) -
t()translation — all user-visible strings uset('namespace.key')fromuseTranslation(), never hardcoded English -
document.title— set viat()so it updates on locale change -
data.tslocale-aware — if the page has adata.tsfile, metric labels / descriptions use translation keys (not raw strings)
import { useState } from 'react';
import { LocaleProvider } from '../../contexts/locale-context';
import Shell from '../../layouts/shell';
import Navigation from '../../components/navigation';
import Breadcrumbs from '../../components/breadcrumbs';
import { initializeTheme, applyTheme, setStoredTheme, type Theme } from '../../utils/theme';
import { initializeLocale, applyLocale, setStoredLocale, type Locale } from '../../utils/locale';
export default function App() {
const [theme, setTheme] = useState<Theme>(() => initializeTheme());
const [locale, setLocale] = useState<Locale>(() => initializeLocale());
const handleThemeChange = (newTheme: Theme) => {
setTheme(newTheme);
applyTheme(newTheme);
setStoredTheme(newTheme);
};
const handleLocaleChange = (newLocale: Locale) => {
setLocale(newLocale);
applyLocale(newLocale);
setStoredLocale(newLocale);
};
return (
<LocaleProvider locale={locale}>
<Shell
theme={theme}
onThemeChange={handleThemeChange}
locale={locale}
onLocaleChange={handleLocaleChange}
breadcrumbs={<Breadcrumbs active={{ text: 'Page Title', href: '/<name>/index.html' }} />}
navigation={<Navigation />}
>
{/* page content */}
</Shell>
</LocaleProvider>
);
}If a page has static data (e.g. metric labels, descriptions, topic names), those strings must be kept as translation keys and resolved at render time — not hardcoded as English strings:
// ❌ Wrong — hardcodes English, bypasses locale
export const metrics = [{ label: 'Active Members', value: 42 }];
// ✅ Correct — return keys, resolve with t() at render time
export const METRIC_KEYS = [{ labelKey: 'home.metrics.activeMembers', value: 42 }];
// In the component:
const { t } = useTranslation();
metrics.map(m => ({ ...m, label: t(m.labelKey) }))- Deep imports only —
import Button from '@cloudscape-design/components/button'(never barrel imports) - No path aliases — all imports use relative paths
- Cloudscape only — no other UI component libraries
- No backend — static site; data fetched at build time and bundled as JSON
See AGENTS.md for the full architectural conventions and constraints.
The site supports two locales, toggled via 🇺🇸↔🇲🇽 in the top navigation:
| Locale | Flag | Description |
|---|---|---|
us |
🇺🇸 | New Mexican English — El Paso Spanglish + local slang |
mx |
🇲🇽 | Chihuahua norteño Spanish — Ciudad Juárez dialect |
Translation files live in src/locales/:
en-US.json— English (source of truth)es-MX.json— Spanish (human-reviewed translations)
Components use the useTranslation() hook:
import { useTranslation } from '../../hooks/useTranslation';
export default function MyComponent() {
const { t } = useTranslation();
return <Header>{t('namespace.headerTitle')}</Header>;
}See LOCALIZATION.md for dialect guides, key naming conventions, and translation workflow.
.github/workflows/deploy.yml triggers on pushes to main that touch src/,
public/, package.json, package-lock.json, vite.config.ts, or tsconfig.json.
It runs: npm install → npm run build → aws s3 sync → CloudFront invalidation.
Required repo secrets (Settings → Secrets and variables → Actions):
| Secret | Purpose |
|---|---|
AWS_ACCESS_KEY_ID |
IAM access key |
AWS_SECRET_ACCESS_KEY |
IAM secret key |
AWS_ROLE_ARN |
IAM role ARN for OIDC (optional if using keys) |
⚠️ If secrets are not configured, the workflow builds successfully but silently skips the S3 deploy and CloudFront invalidation.
aws sso login --profile aerospaceug-admin
npm run lint && npm test && npm run build
aws s3 sync lib/ s3://awsaerospace.org --delete --profile aerospaceug-admin
aws cloudfront create-invalidation \
--distribution-id ECC3LP1BL2CZS \
--paths "/*" \
--profile aerospaceug-admin| Resource | Value |
|---|---|
| AWS CLI profile | aerospaceug-admin (SSO) |
| S3 bucket | awsaerospace.org |
| CloudFront distribution | ECC3LP1BL2CZS |
| CloudFront domain | d2ly3jmh1f74xt.cloudfront.net |
This project uses Squad v0.5.4 with HeraldStack personas for AI-assisted development. Run copilot --agent squad to engage the team. See AGENTS.md for agent roles and routing.
See CONTRIBUTING.md for more information.
The sample code is available under a modified MIT license. See the LICENSE file.