diff --git a/angular-app/src/app/app.config.ts b/angular-app/src/app/app.config.ts
index cb1270e..adcaea5 100644
--- a/angular-app/src/app/app.config.ts
+++ b/angular-app/src/app/app.config.ts
@@ -1,4 +1,5 @@
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
+import { provideHttpClient } from '@angular/common/http';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
@@ -6,6 +7,7 @@ import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
+ provideHttpClient(),
provideRouter(routes)
]
};
diff --git a/angular-app/src/app/app.html b/angular-app/src/app/app.html
deleted file mode 100644
index 7528372..0000000
--- a/angular-app/src/app/app.html
+++ /dev/null
@@ -1,342 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Hello, {{ title() }}
-
Congratulations! Your app is running. 🎉
-
-
-
-
- @for (item of [
- { title: 'Explore the Docs', link: 'https://angular.dev' },
- { title: 'Learn with Tutorials', link: 'https://angular.dev/tutorials' },
- { title: 'Prompt and best practices for AI', link: 'https://angular.dev/ai/develop-with-ai'},
- { title: 'CLI Docs', link: 'https://angular.dev/tools/cli' },
- { title: 'Angular Language Service', link: 'https://angular.dev/tools/language-service' },
- { title: 'Angular DevTools', link: 'https://angular.dev/tools/devtools' },
- ]; track item.title) {
-
- {{ item.title }}
-
-
- }
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/angular-app/src/app/app.routes.ts b/angular-app/src/app/app.routes.ts
index dc39edb..e3540f3 100644
--- a/angular-app/src/app/app.routes.ts
+++ b/angular-app/src/app/app.routes.ts
@@ -1,3 +1,17 @@
import { Routes } from '@angular/router';
+import { CapsulesPage } from './pages/capsules/capsules.page';
+import { LandingpadsPage } from './pages/landingpads/landingpads.page';
+import { LaunchesPage } from './pages/launches/launches.page';
+import { LaunchpadsPage } from './pages/launchpads/launchpads.page';
+import { RocketsPage } from './pages/rockets/rockets.page';
+import { StarlinkPage } from './pages/starlink/starlink.page';
-export const routes: Routes = [];
+export const routes: Routes = [
+ { path: '', component: LaunchesPage },
+ { path: 'rockets', component: RocketsPage },
+ { path: 'capsules', component: CapsulesPage },
+ { path: 'starlink', component: StarlinkPage },
+ { path: 'launchpads', component: LaunchpadsPage },
+ { path: 'landingpads', component: LandingpadsPage },
+ { path: '**', redirectTo: '' }
+];
diff --git a/angular-app/src/app/app.scss b/angular-app/src/app/app.scss
index e69de29..598bd54 100644
--- a/angular-app/src/app/app.scss
+++ b/angular-app/src/app/app.scss
@@ -0,0 +1,98 @@
+.site-header {
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(6px);
+ position: sticky;
+ top: 0;
+ background: rgba(7, 10, 20, 0.85);
+ z-index: 50;
+}
+
+.nav-wrap {
+ min-height: 72px;
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+}
+
+.brand-wrap {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.brand-icon {
+ width: 40px;
+ height: 40px;
+ border-radius: 10px;
+ display: grid;
+ place-items: center;
+ background: #2563eb;
+}
+
+.brand {
+ display: block;
+ font-size: 1.25rem;
+ color: #ffffff;
+}
+
+.brand-wrap small {
+ display: block;
+ color: #94a3b8;
+ font-size: 0.75rem;
+}
+
+.sub-nav {
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+ position: sticky;
+ top: 72px;
+ z-index: 40;
+ backdrop-filter: blur(6px);
+ background: rgba(15, 23, 42, 0.55);
+}
+
+.sub-nav-wrap {
+ display: flex;
+ overflow-x: auto;
+ gap: 2px;
+}
+
+.sub-nav a {
+ color: #94a3b8;
+ font-size: 0.95rem;
+ padding: 14px 16px;
+ border-bottom: 2px solid transparent;
+ white-space: nowrap;
+}
+
+.sub-nav a:hover,
+.sub-nav a.active {
+ color: #ffffff;
+ border-color: #3b82f6;
+}
+
+.page-shell {
+ padding-top: 48px;
+ padding-bottom: 52px;
+}
+
+.site-footer {
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
+ padding: 22px 0;
+ margin-top: 64px;
+}
+
+.footer-wrap {
+ display: flex;
+ justify-content: center;
+}
+
+.footer-wrap p {
+ margin: 0;
+ color: #9aabc8;
+ font-size: 0.9rem;
+}
+
+.footer-wrap a {
+ color: #60a5fa;
+ margin-left: 6px;
+}
diff --git a/angular-app/src/app/app.spec.ts b/angular-app/src/app/app.spec.ts
index dd7f3ea..fcd0024 100644
--- a/angular-app/src/app/app.spec.ts
+++ b/angular-app/src/app/app.spec.ts
@@ -14,10 +14,10 @@ describe('App', () => {
expect(app).toBeTruthy();
});
- it('should render title', async () => {
+ it('should render brand title', async () => {
const fixture = TestBed.createComponent(App);
await fixture.whenStable();
const compiled = fixture.nativeElement as HTMLElement;
- expect(compiled.querySelector('h1')?.textContent).toContain('Hello, angular-app');
+ expect(compiled.querySelector('.brand')?.textContent).toContain('SpaceX Explorer');
});
});
diff --git a/angular-app/src/app/app.ts b/angular-app/src/app/app.ts
index 437d380..0cc9126 100644
--- a/angular-app/src/app/app.ts
+++ b/angular-app/src/app/app.ts
@@ -1,12 +1,48 @@
-import { Component, signal } from '@angular/core';
-import { RouterOutlet } from '@angular/router';
+import { Component } from '@angular/core';
+import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
- imports: [RouterOutlet],
- templateUrl: './app.html',
+ imports: [RouterOutlet, RouterLink, RouterLinkActive],
+ template: `
+
+
+
+
+
+
+
+
+
+ `,
styleUrl: './app.scss'
})
-export class App {
- protected readonly title = signal('angular-app');
-}
+export class App {}
diff --git a/angular-app/src/app/data/mock-data.ts b/angular-app/src/app/data/mock-data.ts
new file mode 100644
index 0000000..ff2dd5c
--- /dev/null
+++ b/angular-app/src/app/data/mock-data.ts
@@ -0,0 +1,391 @@
+export const launches = [
+ {
+ id: '1',
+ name: 'Starlink 4-36',
+ date_utc: '2024-11-15T10:30:00.000Z',
+ success: true,
+ details: 'This mission launched 53 Starlink satellites to low Earth orbit.',
+ rocket: 'Falcon 9',
+ launchpad: 'Kennedy Space Center LC-39A',
+ flight_number: 245,
+ upcoming: false
+ },
+ {
+ id: '2',
+ name: 'Crew-8',
+ date_utc: '2024-10-20T14:15:00.000Z',
+ success: true,
+ details: "NASA's SpaceX Crew-8 mission to the International Space Station.",
+ rocket: 'Falcon 9',
+ launchpad: 'Kennedy Space Center LC-39A',
+ flight_number: 244,
+ upcoming: false
+ },
+ {
+ id: '3',
+ name: 'Starship Flight 5',
+ date_utc: '2024-09-30T08:00:00.000Z',
+ success: true,
+ details: 'Fifth orbital test flight of the Starship super heavy-lift launch vehicle.',
+ rocket: 'Starship',
+ launchpad: 'Starbase',
+ flight_number: 5,
+ upcoming: false
+ },
+ {
+ id: '4',
+ name: 'PACE Mission',
+ date_utc: '2024-08-12T18:45:00.000Z',
+ success: true,
+ details: 'Plankton, Aerosol, Cloud, ocean Ecosystem mission for NASA.',
+ rocket: 'Falcon 9',
+ launchpad: 'Vandenberg SFB SLC-4E',
+ flight_number: 243,
+ upcoming: false
+ },
+ {
+ id: '5',
+ name: 'Starlink 6-15',
+ date_utc: '2026-04-25T12:00:00.000Z',
+ success: null,
+ details: 'Next generation Starlink V2 satellites deployment.',
+ rocket: 'Falcon 9',
+ launchpad: 'Cape Canaveral SLC-40',
+ flight_number: 280,
+ upcoming: true
+ },
+ {
+ id: '6',
+ name: 'Europa Clipper',
+ date_utc: '2026-06-10T09:30:00.000Z',
+ success: null,
+ details: "NASA mission to explore Jupiter's moon Europa.",
+ rocket: 'Falcon Heavy',
+ launchpad: 'Kennedy Space Center LC-39A',
+ flight_number: 12,
+ upcoming: true
+ }
+];
+
+export const rockets = [
+ {
+ id: '1',
+ name: 'Falcon 9',
+ active: true,
+ stages: 2,
+ boosters: 0,
+ cost_per_launch: 67000000,
+ success_rate_pct: 99,
+ first_flight: '2010-06-04',
+ country: 'United States',
+ company: 'SpaceX',
+ height: { meters: 70, feet: 229.6 },
+ diameter: { meters: 3.7, feet: 12 },
+ mass: { kg: 549054, lb: 1207920 },
+ description:
+ 'Falcon 9 is a reusable, two-stage rocket designed and manufactured by SpaceX for the reliable and safe transport of people and payloads into Earth orbit and beyond.'
+ },
+ {
+ id: '2',
+ name: 'Falcon Heavy',
+ active: true,
+ stages: 2,
+ boosters: 2,
+ cost_per_launch: 97000000,
+ success_rate_pct: 100,
+ first_flight: '2018-02-06',
+ country: 'United States',
+ company: 'SpaceX',
+ height: { meters: 70, feet: 229.6 },
+ diameter: { meters: 12.2, feet: 39.9 },
+ mass: { kg: 1420788, lb: 3125735 },
+ description:
+ 'With the ability to lift into orbit nearly 64 metric tons (141,000 lb) Falcon Heavy is the most powerful operational rocket in the world by a factor of two.'
+ },
+ {
+ id: '3',
+ name: 'Starship',
+ active: true,
+ stages: 2,
+ boosters: 0,
+ cost_per_launch: 10000000,
+ success_rate_pct: 80,
+ first_flight: '2023-04-20',
+ country: 'United States',
+ company: 'SpaceX',
+ height: { meters: 120, feet: 394 },
+ diameter: { meters: 9, feet: 30 },
+ mass: { kg: 5000000, lb: 11000000 },
+ description:
+ 'Starship is a fully reusable super heavy-lift launch vehicle under development by SpaceX. It is the largest and most powerful rocket ever built.'
+ },
+ {
+ id: '4',
+ name: 'Falcon 1',
+ active: false,
+ stages: 2,
+ boosters: 0,
+ cost_per_launch: 6700000,
+ success_rate_pct: 40,
+ first_flight: '2006-03-24',
+ country: 'United States',
+ company: 'SpaceX',
+ height: { meters: 22.25, feet: 73 },
+ diameter: { meters: 1.68, feet: 5.5 },
+ mass: { kg: 30146, lb: 66460 },
+ description:
+ 'The Falcon 1 was an expendable launch system privately developed and manufactured by SpaceX during 2006-2009.'
+ }
+];
+
+export const capsules = [
+ {
+ id: '1',
+ serial: 'C201',
+ status: 'active',
+ type: 'Dragon 2',
+ reuse_count: 5,
+ water_landings: 5,
+ land_landings: 0,
+ last_update: 'Capsule successfully recovered after latest mission',
+ launches: ['Crew-1', 'Crew-2', 'Axiom-1', 'Crew-6', 'Axiom-3']
+ },
+ {
+ id: '2',
+ serial: 'C206',
+ status: 'active',
+ type: 'Dragon 2',
+ reuse_count: 3,
+ water_landings: 3,
+ land_landings: 0,
+ last_update: 'Currently docked at ISS',
+ launches: ['CRS-22', 'CRS-26', 'CRS-28']
+ },
+ {
+ id: '3',
+ serial: 'C112',
+ status: 'retired',
+ type: 'Dragon 1',
+ reuse_count: 3,
+ water_landings: 3,
+ land_landings: 0,
+ last_update: 'Retired after successful mission completion',
+ launches: ['CRS-10', 'CRS-13', 'CRS-16']
+ },
+ {
+ id: '4',
+ serial: 'C208',
+ status: 'active',
+ type: 'Dragon 2',
+ reuse_count: 2,
+ water_landings: 2,
+ land_landings: 0,
+ last_update: 'Undergoing refurbishment',
+ launches: ['Crew-4', 'Axiom-2']
+ },
+ {
+ id: '5',
+ serial: 'C210',
+ status: 'unknown',
+ type: 'Dragon 2',
+ reuse_count: 0,
+ water_landings: 0,
+ land_landings: 0,
+ last_update: 'Capsule under construction',
+ launches: []
+ }
+];
+
+export const starlink = [
+ {
+ id: '1',
+ version: 'v2.0 Mini',
+ launch: '2024-11-15',
+ longitude: -122.5,
+ latitude: 37.8,
+ height_km: 550,
+ velocity_kms: 7.59
+ },
+ {
+ id: '2',
+ version: 'v1.5',
+ launch: '2024-09-20',
+ longitude: -95.3,
+ latitude: 29.7,
+ height_km: 540,
+ velocity_kms: 7.61
+ },
+ {
+ id: '3',
+ version: 'v2.0 Mini',
+ launch: '2024-10-05',
+ longitude: 2.3,
+ latitude: 48.8,
+ height_km: 545,
+ velocity_kms: 7.6
+ },
+ {
+ id: '4',
+ version: 'v1.5',
+ launch: '2024-08-12',
+ longitude: 139.6,
+ latitude: 35.6,
+ height_km: 538,
+ velocity_kms: 7.62
+ },
+ {
+ id: '5',
+ version: 'v2.0 Mini',
+ launch: '2024-11-01',
+ longitude: -0.1,
+ latitude: 51.5,
+ height_km: 548,
+ velocity_kms: 7.59
+ },
+ {
+ id: '6',
+ version: 'v1.0',
+ launch: '2023-05-15',
+ longitude: 12.4,
+ latitude: 41.9,
+ height_km: 535,
+ velocity_kms: 7.63
+ }
+];
+
+export const launchpads = [
+ {
+ id: '1',
+ name: 'Kennedy Space Center LC-39A',
+ full_name: 'Kennedy Space Center Historic Launch Complex 39A',
+ status: 'active',
+ locality: 'Cape Canaveral',
+ region: 'Florida',
+ latitude: 28.6,
+ longitude: -80.6,
+ launch_attempts: 187,
+ launch_successes: 185,
+ rockets: ['Falcon 9', 'Falcon Heavy', 'Starship'],
+ details:
+ 'LC-39A has been used for SpaceX launches since 2017. It was previously used for the Saturn V and Space Shuttle launches.'
+ },
+ {
+ id: '2',
+ name: 'Vandenberg SFB SLC-4E',
+ full_name: 'Vandenberg Space Force Base Space Launch Complex 4E',
+ status: 'active',
+ locality: 'Vandenberg',
+ region: 'California',
+ latitude: 34.6,
+ longitude: -120.6,
+ launch_attempts: 95,
+ launch_successes: 94,
+ rockets: ['Falcon 9'],
+ details: "SpaceX's primary West Coast launch site for polar orbit missions."
+ },
+ {
+ id: '3',
+ name: 'Cape Canaveral SLC-40',
+ full_name: 'Cape Canaveral Space Force Station Space Launch Complex 40',
+ status: 'active',
+ locality: 'Cape Canaveral',
+ region: 'Florida',
+ latitude: 28.5,
+ longitude: -80.5,
+ launch_attempts: 156,
+ launch_successes: 154,
+ rockets: ['Falcon 9'],
+ details: "SpaceX's primary East Coast launch site, used for most commercial missions."
+ },
+ {
+ id: '4',
+ name: 'Starbase',
+ full_name: 'SpaceX South Texas Launch Site',
+ status: 'active',
+ locality: 'Boca Chica',
+ region: 'Texas',
+ latitude: 25.9,
+ longitude: -97.1,
+ launch_attempts: 5,
+ launch_successes: 4,
+ rockets: ['Starship'],
+ details: "SpaceX's private launch facility for Starship development and testing."
+ }
+];
+
+export const landingpads = [
+ {
+ id: '1',
+ name: 'LZ-1',
+ full_name: 'Landing Zone 1',
+ status: 'active',
+ type: 'RTLS',
+ locality: 'Cape Canaveral',
+ region: 'Florida',
+ latitude: 28.4,
+ longitude: -80.5,
+ landing_attempts: 45,
+ landing_successes: 44,
+ details:
+ "SpaceX's first East Coast landing pad, located at Cape Canaveral Space Force Station."
+ },
+ {
+ id: '2',
+ name: 'LZ-2',
+ full_name: 'Landing Zone 2',
+ status: 'active',
+ type: 'RTLS',
+ locality: 'Cape Canaveral',
+ region: 'Florida',
+ latitude: 28.4,
+ longitude: -80.5,
+ landing_attempts: 28,
+ landing_successes: 28,
+ details:
+ 'Second East Coast landing pad, used for Falcon Heavy side boosters.'
+ },
+ {
+ id: '3',
+ name: 'LZ-4',
+ full_name: 'Landing Zone 4',
+ status: 'active',
+ type: 'RTLS',
+ locality: 'Vandenberg',
+ region: 'California',
+ latitude: 34.6,
+ longitude: -120.6,
+ landing_attempts: 32,
+ landing_successes: 31,
+ details: "SpaceX's West Coast landing facility at Vandenberg Space Force Base."
+ },
+ {
+ id: '4',
+ name: 'OCISLY',
+ full_name: 'Of Course I Still Love You',
+ status: 'active',
+ type: 'ASDS',
+ locality: 'Atlantic Ocean',
+ region: 'Sea',
+ latitude: null,
+ longitude: null,
+ landing_attempts: 102,
+ landing_successes: 98,
+ details:
+ 'Autonomous spaceport drone ship stationed in the Atlantic Ocean for offshore landings.'
+ },
+ {
+ id: '5',
+ name: 'JRTI',
+ full_name: 'Just Read the Instructions',
+ status: 'active',
+ type: 'ASDS',
+ locality: 'Pacific Ocean',
+ region: 'Sea',
+ latitude: null,
+ longitude: null,
+ landing_attempts: 67,
+ landing_successes: 64,
+ details:
+ 'Autonomous spaceport drone ship stationed in the Pacific Ocean for West Coast missions.'
+ }
+];
diff --git a/angular-app/src/app/pages/contact/contact.page.ts b/angular-app/src/app/pages/contact/contact.page.ts
new file mode 100644
index 0000000..4a19335
--- /dev/null
+++ b/angular-app/src/app/pages/contact/contact.page.ts
@@ -0,0 +1,32 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'app-contact-page',
+ template: `
+
+ Contact
+ Contato
+ Fale com a equipe para parcerias, imprensa e oportunidades técnicas.
+
+
+
+
+ Imprensa
+ media@spacex-data.local
+
+
+ Parcerias
+ partnerships@spacex-data.local
+
+
+ Suporte técnico
+ support@spacex-data.local
+
+
+ Base operacional
+ Hawthorne, Califórnia — Estados Unidos
+
+
+ `
+})
+export class ContactPage {}
diff --git a/angular-app/src/app/pages/data/data.page.ts b/angular-app/src/app/pages/data/data.page.ts
new file mode 100644
index 0000000..e54d7a5
--- /dev/null
+++ b/angular-app/src/app/pages/data/data.page.ts
@@ -0,0 +1,49 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'app-data-page',
+ template: `
+
+ Data Hub
+ Centro de dados espaciais
+ Métricas essenciais de desempenho e histórico de frota.
+
+
+
+
+ Falcon 9
+
+ - Reutilização média: 14 voos por booster
+ - Taxa de sucesso: 99,1%
+ - Cargas em órbita: LEO, GTO, ISS
+
+
+
+
+ Starship
+
+ - Arquitetura totalmente reutilizável
+ - Capacidade alvo: 100t+ para órbita
+ - Foco em missões lunares e marcianas
+
+
+
+
+ Cadência de lançamento
+
+ O volume anual de lançamentos cresce com otimização de turnaround e operação integrada
+ entre produção, integração e recuperação.
+
+
+
+
+ Infraestrutura
+
+ Operações coordenadas entre bases costeiras e terrestres para ampliar capacidade e
+ reduzir janela entre missões.
+
+
+
+ `
+})
+export class DataPage {}
diff --git a/angular-app/src/app/pages/gallery/gallery.page.ts b/angular-app/src/app/pages/gallery/gallery.page.ts
new file mode 100644
index 0000000..db1486a
--- /dev/null
+++ b/angular-app/src/app/pages/gallery/gallery.page.ts
@@ -0,0 +1,22 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'app-gallery-page',
+ template: `
+
+ Visual Archive
+ Galeria
+ Seleção de momentos-chave em solo, decolagem e recuperação.
+
+
+
+ Decolagem noturna
+ Separação de estágio
+ Pouso em droneship
+ Integração de carga
+ Teste estático
+ Controle de missão
+
+ `
+})
+export class GalleryPage {}
diff --git a/angular-app/src/app/pages/home/home.page.ts b/angular-app/src/app/pages/home/home.page.ts
new file mode 100644
index 0000000..f7fab11
--- /dev/null
+++ b/angular-app/src/app/pages/home/home.page.ts
@@ -0,0 +1,37 @@
+import { Component } from '@angular/core';
+import { RouterLink } from '@angular/router';
+
+@Component({
+ selector: 'app-home-page',
+ imports: [RouterLink],
+ template: `
+
+ SpaceX Data Platform
+ Missões, veículos e história espacial em tempo real.
+
+ Explore dados organizados sobre lançamentos, notícias e marcos da engenharia aeroespacial
+ em uma experiência limpa e direta.
+
+
+
+
+
+
+ +380
+ Lançamentos catalogados
+
+
+ 99%
+ Tempo de atividade da plataforma
+
+
+ 24/7
+ Atualização de eventos e telemetria
+
+
+ `
+})
+export class HomePage {}
diff --git a/angular-app/src/app/pages/launches/launches.page.ts b/angular-app/src/app/pages/launches/launches.page.ts
new file mode 100644
index 0000000..564027c
--- /dev/null
+++ b/angular-app/src/app/pages/launches/launches.page.ts
@@ -0,0 +1,110 @@
+import { Component, OnInit, inject } from '@angular/core';
+import { SpacexApiService } from '../../services/spacex-api.service';
+
+@Component({
+ selector: 'app-launches-page',
+ template: `
+
+ SpaceX Launches
+ Explore past and upcoming SpaceX missions
+
+
+ @if (loading) {
+
Loading real data from SpaceX API...
+ }
+ @if (error) {
+ {{ error }}
+ }
+
+
+
+
+
+
+
+
+ @for (launch of filteredLaunches; track launch.id) {
+
+
+
+
{{ launch.name }}
+
{{ formatDate(launch.date_utc) }}
+
+ @if (launch.upcoming) {
+
Upcoming
+ } @else {
+
+ {{ launch.success ? 'Success' : 'Failed' }}
+
+ }
+
+
+ {{ launch.details }}
+
+
+ Flight #{{ launch.flight_number }}
+
+ }
+
+ `
+})
+export class LaunchesPage implements OnInit {
+ private readonly spacexApi = inject(SpacexApiService);
+
+ launches: any[] = [];
+ tab: 'all' | 'upcoming' | 'past' = 'all';
+ loading = true;
+ error = '';
+
+ ngOnInit() {
+ this.spacexApi.getLaunches().subscribe({
+ next: (docs) => {
+ this.launches = docs.map((launch) => ({
+ id: launch.id,
+ name: launch.name,
+ date_utc: launch.date_utc,
+ success: launch.success,
+ details: launch.details || 'No details available.',
+ rocket: typeof launch.rocket === 'object' ? launch.rocket?.name : launch.rocket,
+ launchpad: typeof launch.launchpad === 'object' ? launch.launchpad?.name : launch.launchpad,
+ flight_number: launch.flight_number,
+ upcoming: launch.upcoming
+ }));
+ this.loading = false;
+ },
+ error: () => {
+ this.error = 'Could not load launches from SpaceX API.';
+ this.loading = false;
+ }
+ });
+ }
+
+ get upcomingLaunches() {
+ return this.launches.filter((launch) => launch.upcoming);
+ }
+
+ get pastLaunches() {
+ return this.launches.filter((launch) => !launch.upcoming);
+ }
+
+ get filteredLaunches() {
+ if (this.tab === 'upcoming') return this.upcomingLaunches;
+ if (this.tab === 'past') return this.pastLaunches;
+ return this.launches;
+ }
+
+ formatDate(dateValue: string) {
+ return new Date(dateValue).toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+ }
+}
diff --git a/angular-app/src/app/pages/news/news.page.ts b/angular-app/src/app/pages/news/news.page.ts
new file mode 100644
index 0000000..269a6bc
--- /dev/null
+++ b/angular-app/src/app/pages/news/news.page.ts
@@ -0,0 +1,42 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'app-news-page',
+ template: `
+
+ News Feed
+ Últimas notícias
+ Atualizações de missões, testes e marcos operacionais.
+
+
+
+
+ 11 mar 2026
+ Nova missão de reabastecimento conclui acoplamento com sucesso
+
+ A missão entregou suprimentos críticos e realizou experimentos científicos de longa
+ duração para pesquisa em microgravidade.
+
+
+
+
+ 08 mar 2026
+ Booster reutilizado atinge novo recorde de voos
+
+ O mesmo estágio foi empregado com mínima manutenção, validando ciclos mais rápidos e
+ redução consistente de custo por lançamento.
+
+
+
+
+ 03 mar 2026
+ Teste de infraestrutura amplia janela operacional
+
+ Novas rotinas de preparação diminuem o tempo entre campanhas e aumentam previsibilidade
+ para clientes institucionais e comerciais.
+
+
+
+ `
+})
+export class NewsPage {}
diff --git a/angular-app/src/app/services/spacex-api.service.ts b/angular-app/src/app/services/spacex-api.service.ts
new file mode 100644
index 0000000..9047d7d
--- /dev/null
+++ b/angular-app/src/app/services/spacex-api.service.ts
@@ -0,0 +1,90 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable, inject } from '@angular/core';
+import { Observable, map } from 'rxjs';
+
+type QueryResult = { docs: T[] };
+
+@Injectable({ providedIn: 'root' })
+export class SpacexApiService {
+ private readonly http = inject(HttpClient);
+ private readonly baseUrl = 'https://api.spacexdata.com/v4';
+
+ getLaunches(): Observable {
+ return this.http
+ .post>(`${this.baseUrl}/launches/query`, {
+ query: {},
+ options: {
+ sort: { date_utc: -1 },
+ pagination: false,
+ populate: [
+ { path: 'rocket', select: { name: 1 } },
+ { path: 'launchpad', select: { name: 1 } }
+ ]
+ }
+ })
+ .pipe(map((result) => result.docs ?? []));
+ }
+
+ getRockets(): Observable {
+ return this.http
+ .post>(`${this.baseUrl}/rockets/query`, {
+ query: {},
+ options: { sort: { first_flight: -1 }, pagination: false }
+ })
+ .pipe(map((result) => result.docs ?? []));
+ }
+
+ getCapsules(): Observable {
+ return this.http
+ .post>(`${this.baseUrl}/capsules/query`, {
+ query: {},
+ options: { sort: { serial: 1 }, pagination: false }
+ })
+ .pipe(map((result) => result.docs ?? []));
+ }
+
+ getStarlink(): Observable {
+ return this.http
+ .post>(`${this.baseUrl}/starlink/query`, {
+ query: {},
+ options: {
+ select: {
+ version: 1,
+ longitude: 1,
+ latitude: 1,
+ height_km: 1,
+ velocity_kms: 1,
+ spaceTrack: 1
+ },
+ limit: 120,
+ sort: { launch: -1 }
+ }
+ })
+ .pipe(
+ map((result) =>
+ (result.docs ?? []).filter((item) => item.latitude !== null && item.longitude !== null)
+ )
+ );
+ }
+
+ getLaunchpads(): Observable {
+ return this.http
+ .post>(`${this.baseUrl}/launchpads/query`, {
+ query: {},
+ options: {
+ pagination: false,
+ populate: [{ path: 'rockets', select: { name: 1 } }]
+ }
+ })
+ .pipe(map((result) => result.docs ?? []));
+ }
+
+ getLandpads(): Observable {
+ return this.http
+ .post>(`${this.baseUrl}/landpads/query`, {
+ query: {},
+ options: { pagination: false }
+ })
+ .pipe(map((result) => result.docs ?? []));
+ }
+}
diff --git a/angular-app/src/styles.scss b/angular-app/src/styles.scss
index 90d4ee0..be1886e 100644
--- a/angular-app/src/styles.scss
+++ b/angular-app/src/styles.scss
@@ -1 +1,274 @@
-/* You can add global styles to this file, and also import other style files */
+:root {
+ color-scheme: dark;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html,
+body {
+ margin: 0;
+ min-height: 100%;
+ font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ background:
+ radial-gradient(circle at 15% 20%, rgba(80, 128, 255, 0.16), transparent 34%),
+ radial-gradient(circle at 85% 10%, rgba(74, 219, 255, 0.12), transparent 28%),
+ #050812;
+ color: #f7fbff;
+}
+
+app-root {
+ display: block;
+ min-height: 100vh;
+ background: linear-gradient(135deg, #020617 0%, #0f172a 48%, #020617 100%);
+}
+
+a {
+ color: inherit;
+ text-decoration: none;
+}
+
+.container {
+ width: min(1120px, calc(100% - 2rem));
+ margin-inline: auto;
+}
+
+section + section {
+ margin-top: 24px;
+}
+
+h1,
+h2,
+h3,
+p {
+ margin: 0;
+}
+
+h2 {
+ font-size: clamp(1.7rem, 2.4vw, 2rem);
+ color: #fff;
+}
+
+.page-head p {
+ color: #94a3b8;
+ margin-top: 8px;
+}
+
+.tabs {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ padding: 8px;
+ border: 1px solid #1e293b;
+ border-radius: 12px;
+ background: rgba(15, 23, 42, 0.5);
+}
+
+.tabs button {
+ border: 0;
+ border-radius: 9px;
+ padding: 10px 12px;
+ background: transparent;
+ color: #94a3b8;
+ cursor: pointer;
+}
+
+.tabs button.active {
+ background: #1e293b;
+ color: #f8fafc;
+}
+
+.cards {
+ display: grid;
+ gap: 16px;
+}
+
+.two-cols {
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+}
+
+.three-cols {
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
+}
+
+.four-cols {
+ grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
+}
+
+.panel {
+ border: 1px solid #1e293b;
+ border-radius: 14px;
+ padding: 18px;
+ background: rgba(15, 23, 42, 0.5);
+}
+
+.panel h3 {
+ color: #fff;
+ font-size: 1.25rem;
+}
+
+.muted {
+ color: #94a3b8;
+ line-height: 1.5;
+}
+
+.small {
+ font-size: 0.85rem;
+ color: #94a3b8;
+}
+
+.strong {
+ color: #fff;
+ font-weight: 700;
+}
+
+.row {
+ display: flex;
+ gap: 10px;
+ align-items: center;
+}
+
+.row.start {
+ align-items: flex-start;
+}
+
+.row.between {
+ justify-content: space-between;
+}
+
+.badge {
+ font-size: 0.74rem;
+ border-radius: 999px;
+ padding: 4px 10px;
+ font-weight: 600;
+ text-transform: capitalize;
+ background: #334155;
+ color: #e2e8f0;
+}
+
+.badge-green {
+ background: #16a34a;
+ color: #f0fdf4;
+}
+
+.badge-red {
+ background: #dc2626;
+ color: #fef2f2;
+}
+
+.badge-blue {
+ background: #2563eb;
+ color: #eff6ff;
+}
+
+.badge-purple {
+ background: #7c3aed;
+ color: #f5f3ff;
+}
+
+.badge-yellow {
+ background: #ca8a04;
+ color: #fefce8;
+}
+
+.badge-cyan {
+ background: #0e7490;
+ color: #ecfeff;
+}
+
+.badge-gray {
+ background: #475569;
+ color: #f1f5f9;
+}
+
+.meta-grid {
+ margin-top: 14px;
+ padding-top: 12px;
+ border-top: 1px solid #1e293b;
+ display: grid;
+ gap: 8px 12px;
+}
+
+.cols-2 {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
+.cols-3 {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+}
+
+.meta-grid p {
+ margin: 0;
+ color: #cbd5e1;
+ font-size: 0.92rem;
+}
+
+.progress-wrap {
+ margin-top: 12px;
+}
+
+.progress {
+ margin-top: 8px;
+ width: 100%;
+ height: 8px;
+ background: #0f172a;
+ border-radius: 999px;
+ overflow: hidden;
+}
+
+.progress span {
+ display: block;
+ height: 100%;
+ background: linear-gradient(90deg, #2563eb, #38bdf8);
+}
+
+.chips {
+ margin-top: 14px;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.chip {
+ border: 1px solid #334155;
+ border-radius: 999px;
+ padding: 4px 10px;
+ font-size: 0.8rem;
+ color: #cbd5e1;
+}
+
+.stats .stat {
+ border: 0;
+}
+
+.stat p {
+ color: rgba(255, 255, 255, 0.85);
+ font-size: 0.82rem;
+}
+
+.stat h3 {
+ margin-top: 8px;
+ color: #fff;
+ font-size: 2rem;
+}
+
+.stat.blue {
+ background: linear-gradient(135deg, #2563eb, #1d4ed8);
+}
+
+.stat.purple {
+ background: linear-gradient(135deg, #7c3aed, #6d28d9);
+}
+
+.stat.pink {
+ background: linear-gradient(135deg, #db2777, #be185d);
+}
+
+.stat.cyan {
+ background: linear-gradient(135deg, #0891b2, #0e7490);
+}
+
+.stat.green {
+ background: linear-gradient(135deg, #16a34a, #15803d);
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..73843ee
--- /dev/null
+++ b/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "trabalho-api-workspace",
+ "private": true,
+ "scripts": {
+ "start": "npm --prefix angular-app run start",
+ "build": "npm --prefix angular-app run build",
+ "test": "npm --prefix angular-app run test",
+ "watch": "npm --prefix angular-app run watch"
+ }
+}
diff --git a/reference-site/ATTRIBUTIONS.md b/reference-site/ATTRIBUTIONS.md
new file mode 100644
index 0000000..5df5c40
--- /dev/null
+++ b/reference-site/ATTRIBUTIONS.md
@@ -0,0 +1,3 @@
+This Figma Make file includes components from [shadcn/ui](https://ui.shadcn.com/) used under [MIT license](https://github.com/shadcn-ui/ui/blob/main/LICENSE.md).
+
+This Figma Make file includes photos from [Unsplash](https://unsplash.com) used under [license](https://unsplash.com/license).
diff --git a/reference-site/README.md b/reference-site/README.md
new file mode 100644
index 0000000..19ab865
--- /dev/null
+++ b/reference-site/README.md
@@ -0,0 +1,11 @@
+
+ # SpaceX Data Website
+
+ This is a code bundle for SpaceX Data Website. The original project is available at https://www.figma.com/design/21yJ0y1FSGdnQwlMQoZsUq/SpaceX-Data-Website.
+
+ ## Running the code
+
+ Run `npm i` to install the dependencies.
+
+ Run `npm run dev` to start the development server.
+
\ No newline at end of file
diff --git a/reference-site/guidelines/Guidelines.md b/reference-site/guidelines/Guidelines.md
new file mode 100644
index 0000000..110f117
--- /dev/null
+++ b/reference-site/guidelines/Guidelines.md
@@ -0,0 +1,61 @@
+**Add your own guidelines here**
+
diff --git a/reference-site/index.html b/reference-site/index.html
new file mode 100644
index 0000000..a25a998
--- /dev/null
+++ b/reference-site/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+ SpaceX Data Website
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/reference-site/package.json b/reference-site/package.json
new file mode 100644
index 0000000..c36df6f
--- /dev/null
+++ b/reference-site/package.json
@@ -0,0 +1,89 @@
+{
+ "name": "@figma/my-make-file",
+ "private": true,
+ "version": "0.0.1",
+ "type": "module",
+ "scripts": {
+ "build": "vite build",
+ "dev": "vite"
+ },
+ "dependencies": {
+ "@emotion/react": "11.14.0",
+ "@emotion/styled": "11.14.1",
+ "@mui/icons-material": "7.3.5",
+ "@mui/material": "7.3.5",
+ "@popperjs/core": "2.11.8",
+ "@radix-ui/react-accordion": "1.2.3",
+ "@radix-ui/react-alert-dialog": "1.1.6",
+ "@radix-ui/react-aspect-ratio": "1.1.2",
+ "@radix-ui/react-avatar": "1.1.3",
+ "@radix-ui/react-checkbox": "1.1.4",
+ "@radix-ui/react-collapsible": "1.1.3",
+ "@radix-ui/react-context-menu": "2.2.6",
+ "@radix-ui/react-dialog": "1.1.6",
+ "@radix-ui/react-dropdown-menu": "2.1.6",
+ "@radix-ui/react-hover-card": "1.1.6",
+ "@radix-ui/react-label": "2.1.2",
+ "@radix-ui/react-menubar": "1.1.6",
+ "@radix-ui/react-navigation-menu": "1.2.5",
+ "@radix-ui/react-popover": "1.1.6",
+ "@radix-ui/react-progress": "1.1.2",
+ "@radix-ui/react-radio-group": "1.2.3",
+ "@radix-ui/react-scroll-area": "1.2.3",
+ "@radix-ui/react-select": "2.1.6",
+ "@radix-ui/react-separator": "1.1.2",
+ "@radix-ui/react-slider": "1.2.3",
+ "@radix-ui/react-slot": "1.1.2",
+ "@radix-ui/react-switch": "1.1.3",
+ "@radix-ui/react-tabs": "1.1.3",
+ "@radix-ui/react-toggle-group": "1.1.2",
+ "@radix-ui/react-toggle": "1.1.2",
+ "@radix-ui/react-tooltip": "1.1.8",
+ "class-variance-authority": "0.7.1",
+ "clsx": "2.1.1",
+ "cmdk": "1.1.1",
+ "date-fns": "3.6.0",
+ "embla-carousel-react": "8.6.0",
+ "input-otp": "1.4.2",
+ "lucide-react": "0.487.0",
+ "motion": "12.23.24",
+ "next-themes": "0.4.6",
+ "react-day-picker": "8.10.1",
+ "react-dnd": "16.0.1",
+ "react-dnd-html5-backend": "16.0.1",
+ "react-hook-form": "7.55.0",
+ "react-popper": "2.3.0",
+ "react-resizable-panels": "2.1.7",
+ "react-responsive-masonry": "2.7.1",
+ "react-router": "7.13.0",
+ "react-slick": "0.31.0",
+ "recharts": "2.15.2",
+ "sonner": "2.0.3",
+ "tailwind-merge": "3.2.0",
+ "tw-animate-css": "1.3.8",
+ "vaul": "1.1.2"
+ },
+ "devDependencies": {
+ "@tailwindcss/vite": "4.1.12",
+ "@vitejs/plugin-react": "4.7.0",
+ "tailwindcss": "4.1.12",
+ "vite": "6.3.5"
+ },
+ "peerDependencies": {
+ "react": "18.3.1",
+ "react-dom": "18.3.1"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ },
+ "pnpm": {
+ "overrides": {
+ "vite": "6.3.5"
+ }
+ }
+}
\ No newline at end of file
diff --git a/reference-site/postcss.config.mjs b/reference-site/postcss.config.mjs
new file mode 100644
index 0000000..531dbec
--- /dev/null
+++ b/reference-site/postcss.config.mjs
@@ -0,0 +1,15 @@
+/**
+ * PostCSS Configuration
+ *
+ * Tailwind CSS v4 (via @tailwindcss/vite) automatically sets up all required
+ * PostCSS plugins — you do NOT need to include `tailwindcss` or `autoprefixer` here.
+ *
+ * This file only exists for adding additional PostCSS plugins, if needed.
+ * For example:
+ *
+ * import postcssNested from 'postcss-nested'
+ * export default { plugins: [postcssNested()] }
+ *
+ * Otherwise, you can leave this file empty.
+ */
+export default {}
diff --git a/reference-site/src/app/App.tsx b/reference-site/src/app/App.tsx
new file mode 100644
index 0000000..3b3eb8d
--- /dev/null
+++ b/reference-site/src/app/App.tsx
@@ -0,0 +1,6 @@
+import { RouterProvider } from "react-router";
+import { router } from "./routes";
+
+export default function App() {
+ return ;
+}
\ No newline at end of file
diff --git a/reference-site/src/app/components/Layout.tsx b/reference-site/src/app/components/Layout.tsx
new file mode 100644
index 0000000..bb006ef
--- /dev/null
+++ b/reference-site/src/app/components/Layout.tsx
@@ -0,0 +1,85 @@
+import { Link, Outlet, useLocation } from "react-router";
+import { Rocket, Satellite, Package, Radio, MapPin, Target } from "lucide-react";
+
+export function Layout() {
+ const location = useLocation();
+
+ const navItems = [
+ { path: "/", label: "Launches", icon: Rocket },
+ { path: "/rockets", label: "Rockets", icon: Rocket },
+ { path: "/capsules", label: "Capsules", icon: Package },
+ { path: "/starlink", label: "Starlink", icon: Satellite },
+ { path: "/launchpads", label: "Launchpads", icon: MapPin },
+ { path: "/landingpads", label: "Landing Pads", icon: Target },
+ ];
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+
+
+
+
SpaceX Explorer
+
Launch & Mission Data
+
+
+
+
+
+
+ {/* Navigation */}
+
+
+ {/* Main Content */}
+
+
+
+
+ {/* Footer */}
+
+
+ );
+}
diff --git a/reference-site/src/app/components/figma/ImageWithFallback.tsx b/reference-site/src/app/components/figma/ImageWithFallback.tsx
new file mode 100644
index 0000000..0e26139
--- /dev/null
+++ b/reference-site/src/app/components/figma/ImageWithFallback.tsx
@@ -0,0 +1,27 @@
+import React, { useState } from 'react'
+
+const ERROR_IMG_SRC =
+ 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODgiIGhlaWdodD0iODgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBvcGFjaXR5PSIuMyIgZmlsbD0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIzLjciPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjU2IiBoZWlnaHQ9IjU2IiByeD0iNiIvPjxwYXRoIGQ9Im0xNiA1OCAxNi0xOCAzMiAzMiIvPjxjaXJjbGUgY3g9IjUzIiBjeT0iMzUiIHI9IjciLz48L3N2Zz4KCg=='
+
+export function ImageWithFallback(props: React.ImgHTMLAttributes) {
+ const [didError, setDidError] = useState(false)
+
+ const handleError = () => {
+ setDidError(true)
+ }
+
+ const { src, alt, style, className, ...rest } = props
+
+ return didError ? (
+
+
+

+
+
+ ) : (
+
+ )
+}
diff --git a/reference-site/src/app/components/ui/accordion.tsx b/reference-site/src/app/components/ui/accordion.tsx
new file mode 100644
index 0000000..bd6b1e3
--- /dev/null
+++ b/reference-site/src/app/components/ui/accordion.tsx
@@ -0,0 +1,66 @@
+"use client";
+
+import * as React from "react";
+import * as AccordionPrimitive from "@radix-ui/react-accordion";
+import { ChevronDownIcon } from "lucide-react";
+
+import { cn } from "./utils";
+
+function Accordion({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function AccordionItem({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function AccordionTrigger({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ svg]:rotate-180",
+ className,
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+ );
+}
+
+function AccordionContent({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
diff --git a/reference-site/src/app/components/ui/alert-dialog.tsx b/reference-site/src/app/components/ui/alert-dialog.tsx
new file mode 100644
index 0000000..875b8df
--- /dev/null
+++ b/reference-site/src/app/components/ui/alert-dialog.tsx
@@ -0,0 +1,157 @@
+"use client";
+
+import * as React from "react";
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
+
+import { cn } from "./utils";
+import { buttonVariants } from "./button";
+
+function AlertDialog({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function AlertDialogTrigger({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function AlertDialogPortal({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function AlertDialogOverlay({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function AlertDialogContent({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+ );
+}
+
+function AlertDialogHeader({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function AlertDialogFooter({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function AlertDialogTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function AlertDialogDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function AlertDialogAction({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function AlertDialogCancel({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+export {
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel,
+};
diff --git a/reference-site/src/app/components/ui/alert.tsx b/reference-site/src/app/components/ui/alert.tsx
new file mode 100644
index 0000000..9c35976
--- /dev/null
+++ b/reference-site/src/app/components/ui/alert.tsx
@@ -0,0 +1,66 @@
+import * as React from "react";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "./utils";
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
+ {
+ variants: {
+ variant: {
+ default: "bg-card text-card-foreground",
+ destructive:
+ "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ },
+);
+
+function Alert({
+ className,
+ variant,
+ ...props
+}: React.ComponentProps<"div"> & VariantProps) {
+ return (
+
+ );
+}
+
+function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function AlertDescription({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+export { Alert, AlertTitle, AlertDescription };
diff --git a/reference-site/src/app/components/ui/aspect-ratio.tsx b/reference-site/src/app/components/ui/aspect-ratio.tsx
new file mode 100644
index 0000000..c16d6bc
--- /dev/null
+++ b/reference-site/src/app/components/ui/aspect-ratio.tsx
@@ -0,0 +1,11 @@
+"use client";
+
+import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
+
+function AspectRatio({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+export { AspectRatio };
diff --git a/reference-site/src/app/components/ui/avatar.tsx b/reference-site/src/app/components/ui/avatar.tsx
new file mode 100644
index 0000000..c990451
--- /dev/null
+++ b/reference-site/src/app/components/ui/avatar.tsx
@@ -0,0 +1,53 @@
+"use client";
+
+import * as React from "react";
+import * as AvatarPrimitive from "@radix-ui/react-avatar";
+
+import { cn } from "./utils";
+
+function Avatar({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function AvatarImage({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function AvatarFallback({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+export { Avatar, AvatarImage, AvatarFallback };
diff --git a/reference-site/src/app/components/ui/badge.tsx b/reference-site/src/app/components/ui/badge.tsx
new file mode 100644
index 0000000..2ccc2c4
--- /dev/null
+++ b/reference-site/src/app/components/ui/badge.tsx
@@ -0,0 +1,46 @@
+import * as React from "react";
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "./utils";
+
+const badgeVariants = cva(
+ "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
+ destructive:
+ "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+ outline:
+ "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ },
+);
+
+function Badge({
+ className,
+ variant,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"span"> &
+ VariantProps & { asChild?: boolean }) {
+ const Comp = asChild ? Slot : "span";
+
+ return (
+
+ );
+}
+
+export { Badge, badgeVariants };
diff --git a/reference-site/src/app/components/ui/breadcrumb.tsx b/reference-site/src/app/components/ui/breadcrumb.tsx
new file mode 100644
index 0000000..8f84d7e
--- /dev/null
+++ b/reference-site/src/app/components/ui/breadcrumb.tsx
@@ -0,0 +1,109 @@
+import * as React from "react";
+import { Slot } from "@radix-ui/react-slot";
+import { ChevronRight, MoreHorizontal } from "lucide-react";
+
+import { cn } from "./utils";
+
+function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
+ return ;
+}
+
+function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
+ return (
+
+ );
+}
+
+function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
+ return (
+
+ );
+}
+
+function BreadcrumbLink({
+ asChild,
+ className,
+ ...props
+}: React.ComponentProps<"a"> & {
+ asChild?: boolean;
+}) {
+ const Comp = asChild ? Slot : "a";
+
+ return (
+
+ );
+}
+
+function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
+ return (
+
+ );
+}
+
+function BreadcrumbSeparator({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<"li">) {
+ return (
+ svg]:size-3.5", className)}
+ {...props}
+ >
+ {children ?? }
+
+ );
+}
+
+function BreadcrumbEllipsis({
+ className,
+ ...props
+}: React.ComponentProps<"span">) {
+ return (
+
+
+ More
+
+ );
+}
+
+export {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis,
+};
diff --git a/reference-site/src/app/components/ui/button.tsx b/reference-site/src/app/components/ui/button.tsx
new file mode 100644
index 0000000..40ef7aa
--- /dev/null
+++ b/reference-site/src/app/components/ui/button.tsx
@@ -0,0 +1,58 @@
+import * as React from "react";
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "./utils";
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+ outline:
+ "border bg-background text-foreground hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost:
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
+ icon: "size-9 rounded-md",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ },
+);
+
+function Button({
+ className,
+ variant,
+ size,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"button"> &
+ VariantProps & {
+ asChild?: boolean;
+ }) {
+ const Comp = asChild ? Slot : "button";
+
+ return (
+
+ );
+}
+
+export { Button, buttonVariants };
diff --git a/reference-site/src/app/components/ui/calendar.tsx b/reference-site/src/app/components/ui/calendar.tsx
new file mode 100644
index 0000000..ee7b73f
--- /dev/null
+++ b/reference-site/src/app/components/ui/calendar.tsx
@@ -0,0 +1,75 @@
+"use client";
+
+import * as React from "react";
+import { ChevronLeft, ChevronRight } from "lucide-react";
+import { DayPicker } from "react-day-picker";
+
+import { cn } from "./utils";
+import { buttonVariants } from "./button";
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ ...props
+}: React.ComponentProps) {
+ return (
+ .day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
+ : "[&:has([aria-selected])]:rounded-md",
+ ),
+ day: cn(
+ buttonVariants({ variant: "ghost" }),
+ "size-8 p-0 font-normal aria-selected:opacity-100",
+ ),
+ day_range_start:
+ "day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground",
+ day_range_end:
+ "day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground",
+ day_selected:
+ "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
+ day_today: "bg-accent text-accent-foreground",
+ day_outside:
+ "day-outside text-muted-foreground aria-selected:text-muted-foreground",
+ day_disabled: "text-muted-foreground opacity-50",
+ day_range_middle:
+ "aria-selected:bg-accent aria-selected:text-accent-foreground",
+ day_hidden: "invisible",
+ ...classNames,
+ }}
+ components={{
+ IconLeft: ({ className, ...props }) => (
+
+ ),
+ IconRight: ({ className, ...props }) => (
+
+ ),
+ }}
+ {...props}
+ />
+ );
+}
+
+export { Calendar };
diff --git a/reference-site/src/app/components/ui/card.tsx b/reference-site/src/app/components/ui/card.tsx
new file mode 100644
index 0000000..5f9d58a
--- /dev/null
+++ b/reference-site/src/app/components/ui/card.tsx
@@ -0,0 +1,92 @@
+import * as React from "react";
+
+import { cn } from "./utils";
+
+function Card({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function CardAction({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function CardContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+export {
+ Card,
+ CardHeader,
+ CardFooter,
+ CardTitle,
+ CardAction,
+ CardDescription,
+ CardContent,
+};
diff --git a/reference-site/src/app/components/ui/carousel.tsx b/reference-site/src/app/components/ui/carousel.tsx
new file mode 100644
index 0000000..bb5ab13
--- /dev/null
+++ b/reference-site/src/app/components/ui/carousel.tsx
@@ -0,0 +1,241 @@
+"use client";
+
+import * as React from "react";
+import useEmblaCarousel, {
+ type UseEmblaCarouselType,
+} from "embla-carousel-react";
+import { ArrowLeft, ArrowRight } from "lucide-react";
+
+import { cn } from "./utils";
+import { Button } from "./button";
+
+type CarouselApi = UseEmblaCarouselType[1];
+type UseCarouselParameters = Parameters;
+type CarouselOptions = UseCarouselParameters[0];
+type CarouselPlugin = UseCarouselParameters[1];
+
+type CarouselProps = {
+ opts?: CarouselOptions;
+ plugins?: CarouselPlugin;
+ orientation?: "horizontal" | "vertical";
+ setApi?: (api: CarouselApi) => void;
+};
+
+type CarouselContextProps = {
+ carouselRef: ReturnType[0];
+ api: ReturnType[1];
+ scrollPrev: () => void;
+ scrollNext: () => void;
+ canScrollPrev: boolean;
+ canScrollNext: boolean;
+} & CarouselProps;
+
+const CarouselContext = React.createContext(null);
+
+function useCarousel() {
+ const context = React.useContext(CarouselContext);
+
+ if (!context) {
+ throw new Error("useCarousel must be used within a ");
+ }
+
+ return context;
+}
+
+function Carousel({
+ orientation = "horizontal",
+ opts,
+ setApi,
+ plugins,
+ className,
+ children,
+ ...props
+}: React.ComponentProps<"div"> & CarouselProps) {
+ const [carouselRef, api] = useEmblaCarousel(
+ {
+ ...opts,
+ axis: orientation === "horizontal" ? "x" : "y",
+ },
+ plugins,
+ );
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false);
+ const [canScrollNext, setCanScrollNext] = React.useState(false);
+
+ const onSelect = React.useCallback((api: CarouselApi) => {
+ if (!api) return;
+ setCanScrollPrev(api.canScrollPrev());
+ setCanScrollNext(api.canScrollNext());
+ }, []);
+
+ const scrollPrev = React.useCallback(() => {
+ api?.scrollPrev();
+ }, [api]);
+
+ const scrollNext = React.useCallback(() => {
+ api?.scrollNext();
+ }, [api]);
+
+ const handleKeyDown = React.useCallback(
+ (event: React.KeyboardEvent) => {
+ if (event.key === "ArrowLeft") {
+ event.preventDefault();
+ scrollPrev();
+ } else if (event.key === "ArrowRight") {
+ event.preventDefault();
+ scrollNext();
+ }
+ },
+ [scrollPrev, scrollNext],
+ );
+
+ React.useEffect(() => {
+ if (!api || !setApi) return;
+ setApi(api);
+ }, [api, setApi]);
+
+ React.useEffect(() => {
+ if (!api) return;
+ onSelect(api);
+ api.on("reInit", onSelect);
+ api.on("select", onSelect);
+
+ return () => {
+ api?.off("select", onSelect);
+ };
+ }, [api, onSelect]);
+
+ return (
+
+
+ {children}
+
+
+ );
+}
+
+function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
+ const { carouselRef, orientation } = useCarousel();
+
+ return (
+
+ );
+}
+
+function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
+ const { orientation } = useCarousel();
+
+ return (
+
+ );
+}
+
+function CarouselPrevious({
+ className,
+ variant = "outline",
+ size = "icon",
+ ...props
+}: React.ComponentProps) {
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel();
+
+ return (
+
+ );
+}
+
+function CarouselNext({
+ className,
+ variant = "outline",
+ size = "icon",
+ ...props
+}: React.ComponentProps) {
+ const { orientation, scrollNext, canScrollNext } = useCarousel();
+
+ return (
+
+ );
+}
+
+export {
+ type CarouselApi,
+ Carousel,
+ CarouselContent,
+ CarouselItem,
+ CarouselPrevious,
+ CarouselNext,
+};
diff --git a/reference-site/src/app/components/ui/chart.tsx b/reference-site/src/app/components/ui/chart.tsx
new file mode 100644
index 0000000..b49bc36
--- /dev/null
+++ b/reference-site/src/app/components/ui/chart.tsx
@@ -0,0 +1,353 @@
+"use client";
+
+import * as React from "react";
+import * as RechartsPrimitive from "recharts";
+
+import { cn } from "./utils";
+
+// Format: { THEME_NAME: CSS_SELECTOR }
+const THEMES = { light: "", dark: ".dark" } as const;
+
+export type ChartConfig = {
+ [k in string]: {
+ label?: React.ReactNode;
+ icon?: React.ComponentType;
+ } & (
+ | { color?: string; theme?: never }
+ | { color?: never; theme: Record }
+ );
+};
+
+type ChartContextProps = {
+ config: ChartConfig;
+};
+
+const ChartContext = React.createContext(null);
+
+function useChart() {
+ const context = React.useContext(ChartContext);
+
+ if (!context) {
+ throw new Error("useChart must be used within a ");
+ }
+
+ return context;
+}
+
+function ChartContainer({
+ id,
+ className,
+ children,
+ config,
+ ...props
+}: React.ComponentProps<"div"> & {
+ config: ChartConfig;
+ children: React.ComponentProps<
+ typeof RechartsPrimitive.ResponsiveContainer
+ >["children"];
+}) {
+ const uniqueId = React.useId();
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
+
+ return (
+
+
+
+
+ {children}
+
+
+
+ );
+}
+
+const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
+ const colorConfig = Object.entries(config).filter(
+ ([, config]) => config.theme || config.color,
+ );
+
+ if (!colorConfig.length) {
+ return null;
+ }
+
+ return (
+