Initial commit
This commit is contained in:
10
apps/shop/src/app/app.config.server.ts
Normal file
10
apps/shop/src/app/app.config.server.ts
Normal 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);
|
||||
22
apps/shop/src/app/app.config.ts
Normal file
22
apps/shop/src/app/app.config.ts
Normal 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
66
apps/shop/src/app/app.css
Normal 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;
|
||||
}
|
||||
19
apps/shop/src/app/app.html
Normal file
19
apps/shop/src/app/app.html
Normal 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>
|
||||
12
apps/shop/src/app/app.routes.server.ts
Normal file
12
apps/shop/src/app/app.routes.server.ts
Normal 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,
|
||||
},
|
||||
];
|
||||
25
apps/shop/src/app/app.routes.ts
Normal file
25
apps/shop/src/app/app.routes.ts
Normal 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',
|
||||
},
|
||||
];
|
||||
58
apps/shop/src/app/app.spec.ts
Normal file
58
apps/shop/src/app/app.spec.ts
Normal 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
13
apps/shop/src/app/app.ts
Normal 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
13
apps/shop/src/index.html
Normal 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>
|
||||
7
apps/shop/src/main.server.ts
Normal file
7
apps/shop/src/main.server.ts
Normal 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
5
apps/shop/src/main.ts
Normal 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
66
apps/shop/src/server.ts
Normal 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
38
apps/shop/src/styles.css
Normal 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;
|
||||
}
|
||||
13
apps/shop/src/test-setup.ts
Normal file
13
apps/shop/src/test-setup.ts
Normal 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()
|
||||
);
|
||||
Reference in New Issue
Block a user