Initial commit

This commit is contained in:
2026-07-27 18:01:07 +02:00
commit 0758b00d54
192 changed files with 45091 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
import { provideServerRendering, withRoutes } from '@angular/ssr';
import { appConfig } from './app.config';
import { serverRoutes } from './app.routes.server';
const serverConfig: ApplicationConfig = {
providers: [provideServerRendering(withRoutes(serverRoutes))],
};
export const config = mergeApplicationConfig(appConfig, serverConfig);

View File

@@ -0,0 +1,22 @@
import {
ApplicationConfig,
provideBrowserGlobalErrorListeners,
provideZoneChangeDetection,
} from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withFetch } from '@angular/common/http';
import { appRoutes } from './app.routes';
import {
provideClientHydration,
withEventReplay,
} from '@angular/platform-browser';
export const appConfig: ApplicationConfig = {
providers: [
provideClientHydration(withEventReplay()),
provideBrowserGlobalErrorListeners(),
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(appRoutes),
provideHttpClient(withFetch()),
],
};

66
apps/shop/src/app/app.css Normal file
View File

@@ -0,0 +1,66 @@
.app-header {
background: #2c3e50;
color: white;
padding: 1rem 0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.app-header .container {
max-width: 1200px;
margin: 0 auto;
padding: 0 24px;
display: flex;
justify-content: space-between;
align-items: center;
}
.app-header h1 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
}
.app-header nav {
display: flex;
gap: 24px;
}
.app-header nav a {
color: white;
text-decoration: none;
font-size: 1rem;
padding: 8px 16px;
border-radius: 4px;
transition: background 0.3s;
}
.app-header nav a:hover {
background: rgba(255, 255, 255, 0.1);
}
.app-header nav a.active {
background: #3498db;
}
.app-main {
min-height: calc(100vh - 140px);
background: #f5f5f5;
}
.app-footer {
background: #34495e;
color: white;
padding: 24px 0;
text-align: center;
}
.app-footer .container {
max-width: 1200px;
margin: 0 auto;
padding: 0 24px;
}
.app-footer p {
margin: 8px 0;
font-size: 0.9rem;
}

View File

@@ -0,0 +1,19 @@
<header class="app-header">
<div class="container">
<h1>{{ title }}</h1>
<nav>
<a routerLink="/products" routerLinkActive="active">Products</a>
</nav>
</div>
</header>
<main class="app-main">
<router-outlet></router-outlet>
</main>
<footer class="app-footer">
<div class="container">
<p>© 2025 Nx Shop Demo - Showcasing Nx Monorepo Features</p>
<p>Frontend (Angular) + Backend (Express) + Shared Libraries</p>
</div>
</footer>

View File

@@ -0,0 +1,12 @@
import { RenderMode, ServerRoute } from '@angular/ssr';
export const serverRoutes: ServerRoute[] = [
{
path: 'products/:id',
renderMode: RenderMode.Server,
},
{
path: '**',
renderMode: RenderMode.Server,
},
];

View File

@@ -0,0 +1,25 @@
import { Route } from '@angular/router';
export const appRoutes: Route[] = [
{
path: '',
redirectTo: 'products',
pathMatch: 'full',
},
{
path: 'products',
loadChildren: () =>
import('@org/shop/feature-products').then(m => m.featureProductsRoutes),
},
{
path: 'products',
loadChildren: () =>
import('@org/shop/feature-product-detail').then(
m => m.featureProductDetailRoutes
),
},
{
path: '**',
redirectTo: 'products',
},
];

View File

@@ -0,0 +1,58 @@
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { App } from './app';
import { appRoutes } from './app.routes';
import { provideRouter } from '@angular/router';
import { By } from '@angular/platform-browser';
describe('App', () => {
let component: App;
let fixture: ComponentFixture<App>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [App],
providers: [provideRouter(appRoutes)],
}).compileComponents();
fixture = TestBed.createComponent(App);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create the app component', () => {
expect(component).toBeTruthy();
});
it('should render title in header', () => {
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Nx Shop Demo');
});
it('should render navigation links', () => {
const navLinks = fixture.debugElement.queryAll(By.css('nav a'));
expect(navLinks.length).toBeGreaterThan(0);
expect(navLinks[0].nativeElement.textContent).toContain('Products');
expect(navLinks[0].nativeElement.getAttribute('routerLink')).toBe(
'/products'
);
});
it('should render footer with correct copyright', () => {
const footer = fixture.nativeElement.querySelector('.app-footer');
expect(footer).toBeTruthy();
expect(footer?.textContent).toContain('© 2025 Nx Shop Demo');
expect(footer?.textContent).toContain(
'Frontend (Angular) + Backend (Express) + Shared Libraries'
);
});
it('should have router outlet for dynamic content', () => {
const routerOutlet = fixture.nativeElement.querySelector('router-outlet');
expect(routerOutlet).toBeTruthy();
});
it('should apply change detection strategy OnPush', () => {
const metadata = (App as unknown as { ɵcmp: { onPush: boolean } })['ɵcmp'];
expect(metadata.onPush).toBeTruthy();
});
});

13
apps/shop/src/app/app.ts Normal file
View File

@@ -0,0 +1,13 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { RouterModule } from '@angular/router';
@Component({
imports: [RouterModule],
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
protected title = 'Nx Shop Demo';
}

13
apps/shop/src/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>shop</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>
<body>
<app-root></app-root>
</body>
</html>

View File

@@ -0,0 +1,7 @@
import { bootstrapApplication, BootstrapContext } from '@angular/platform-browser';
import { App } from './app/app';
import { config } from './app/app.config.server';
const bootstrap = (context: BootstrapContext) => bootstrapApplication(App, config, context);
export default bootstrap;

5
apps/shop/src/main.ts Normal file
View File

@@ -0,0 +1,5 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { App } from './app/app';
bootstrapApplication(App, appConfig).catch((err) => console.error(err));

66
apps/shop/src/server.ts Normal file
View File

@@ -0,0 +1,66 @@
import {
AngularNodeAppEngine,
createNodeRequestHandler,
isMainModule,
writeResponseToNodeResponse,
} from '@angular/ssr/node';
import express from 'express';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const serverDistFolder = dirname(fileURLToPath(import.meta.url));
const browserDistFolder = resolve(serverDistFolder, '../browser');
const app = express();
const angularApp = new AngularNodeAppEngine();
/**
* Example Express Rest API endpoints can be defined here.
* Uncomment and define endpoints as necessary.
*
* Example:
* ```ts
* app.get('/api/**', (req, res) => {
* // Handle API request
* });
* ```
*/
/**
* Serve static files from /browser
*/
app.use(
express.static(browserDistFolder, {
maxAge: '1y',
index: false,
redirect: false,
})
);
/**
* Handle all other requests by rendering the Angular application.
*/
app.use('/**', (req, res, next) => {
angularApp
.handle(req)
.then((response) =>
response ? writeResponseToNodeResponse(response, res) : next()
)
.catch(next);
});
/**
* Start the server if this module is the main entry point.
* The server listens on the port defined by the `PORT` environment variable, or defaults to 4000.
*/
if (isMainModule(import.meta.url)) {
const port = process.env['PORT'] || 4000;
app.listen(port, () => {
console.log(`Node Express server listening on http://localhost:${port}`);
});
}
/**
* Request handler used by the Angular CLI (for dev-server and during build) or Firebase Cloud Functions.
*/
export const reqHandler = createNodeRequestHandler(app);

38
apps/shop/src/styles.css Normal file
View File

@@ -0,0 +1,38 @@
/* Global Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
line-height: 1.6;
color: #333;
background: #f5f5f5;
}
a {
color: #3498db;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
button {
font-family: inherit;
}
input,
select,
textarea {
font-family: inherit;
}
img {
max-width: 100%;
height: auto;
}

View File

@@ -0,0 +1,13 @@
import '@angular/compiler';
import '@analogjs/vitest-angular/setup-zone';
import {
BrowserTestingModule,
platformBrowserTesting,
} from '@angular/platform-browser/testing';
import { getTestBed } from '@angular/core/testing';
getTestBed().initTestEnvironment(
BrowserTestingModule,
platformBrowserTesting()
);