Backend & Frontend mit Mail Versand
This commit is contained in:
parent
c62edc9cc1
commit
368257633b
|
|
@ -40,3 +40,8 @@ testem.log
|
||||||
# System files
|
# System files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.nx/cache
|
||||||
|
.nx/workspace-data
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Add files here to ignore them from prettier formatting
|
||||||
|
/dist
|
||||||
|
/coverage
|
||||||
|
/.nx/cache
|
||||||
|
/.nx/workspace-data
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
{
|
{
|
||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
|
"recommendations": [
|
||||||
"recommendations": ["angular.ng-template"]
|
"angular.ng-template",
|
||||||
|
"nrwl.angular-console",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"firsttris.vscode-jest-runner"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
105
angular.json
105
angular.json
|
|
@ -1,105 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
|
||||||
"version": 1,
|
|
||||||
"newProjectRoot": "projects",
|
|
||||||
"projects": {
|
|
||||||
"bay-area-affiliates__": {
|
|
||||||
"projectType": "application",
|
|
||||||
"schematics": {
|
|
||||||
"@schematics/angular:component": {
|
|
||||||
"inlineTemplate": true,
|
|
||||||
"inlineStyle": true,
|
|
||||||
"skipTests": true
|
|
||||||
},
|
|
||||||
"@schematics/angular:class": {
|
|
||||||
"skipTests": true
|
|
||||||
},
|
|
||||||
"@schematics/angular:directive": {
|
|
||||||
"skipTests": true
|
|
||||||
},
|
|
||||||
"@schematics/angular:guard": {
|
|
||||||
"skipTests": true
|
|
||||||
},
|
|
||||||
"@schematics/angular:interceptor": {
|
|
||||||
"skipTests": true
|
|
||||||
},
|
|
||||||
"@schematics/angular:pipe": {
|
|
||||||
"skipTests": true
|
|
||||||
},
|
|
||||||
"@schematics/angular:resolver": {
|
|
||||||
"skipTests": true
|
|
||||||
},
|
|
||||||
"@schematics/angular:service": {
|
|
||||||
"skipTests": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": "",
|
|
||||||
"sourceRoot": "src",
|
|
||||||
"prefix": "app",
|
|
||||||
"architect": {
|
|
||||||
"build": {
|
|
||||||
"builder": "@angular-devkit/build-angular:application",
|
|
||||||
"options": {
|
|
||||||
"outputPath": "dist/bay-area-affiliates__",
|
|
||||||
"index": "src/index.html",
|
|
||||||
"browser": "src/main.ts",
|
|
||||||
"polyfills": [
|
|
||||||
"zone.js"
|
|
||||||
],
|
|
||||||
"tsConfig": "tsconfig.app.json",
|
|
||||||
"assets": [
|
|
||||||
"src/assets",
|
|
||||||
{
|
|
||||||
"glob": "**/*",
|
|
||||||
"input": "public"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"styles": [
|
|
||||||
"src/styles.css",
|
|
||||||
"node_modules/aos/dist/aos.css"
|
|
||||||
],
|
|
||||||
"scripts": []
|
|
||||||
},
|
|
||||||
"configurations": {
|
|
||||||
"production": {
|
|
||||||
"budgets": [
|
|
||||||
{
|
|
||||||
"type": "initial",
|
|
||||||
"maximumWarning": "500kB",
|
|
||||||
"maximumError": "1MB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "anyComponentStyle",
|
|
||||||
"maximumWarning": "2kB",
|
|
||||||
"maximumError": "4kB"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"outputHashing": "all"
|
|
||||||
},
|
|
||||||
"development": {
|
|
||||||
"optimization": false,
|
|
||||||
"extractLicenses": false,
|
|
||||||
"sourceMap": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"defaultConfiguration": "production"
|
|
||||||
},
|
|
||||||
"serve": {
|
|
||||||
"builder": "@angular-devkit/build-angular:dev-server",
|
|
||||||
"configurations": {
|
|
||||||
"production": {
|
|
||||||
"buildTarget": "bay-area-affiliates__:build:production"
|
|
||||||
},
|
|
||||||
"development": {
|
|
||||||
"buildTarget": "bay-area-affiliates__:build:development"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"defaultConfiguration": "development"
|
|
||||||
},
|
|
||||||
"extract-i18n": {
|
|
||||||
"builder": "@angular-devkit/build-angular:extract-i18n"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
const baseConfig = require('../eslint.config.js');
|
||||||
|
|
||||||
|
module.exports = [...baseConfig];
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"name": "api",
|
||||||
|
"$schema": "../node_modules/nx/schemas/project-schema.json",
|
||||||
|
"sourceRoot": "api/src",
|
||||||
|
"projectType": "application",
|
||||||
|
"tags": [],
|
||||||
|
"targets": {
|
||||||
|
"serve": {
|
||||||
|
"executor": "@nx/js:node",
|
||||||
|
"defaultConfiguration": "development",
|
||||||
|
"dependsOn": ["build"],
|
||||||
|
"options": {
|
||||||
|
"buildTarget": "api:build",
|
||||||
|
"runBuildTargetDependencies": false
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"development": {
|
||||||
|
"buildTarget": "api:build:development"
|
||||||
|
},
|
||||||
|
"production": {
|
||||||
|
"buildTarget": "api:build:production"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { Body, Controller, Get, Post } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
|
||||||
|
@Controller()
|
||||||
|
export class AppController {
|
||||||
|
constructor(private readonly appService: AppService) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
async sendEMail(@Body() mailInfo: {name:string,email:String,message:string}): Promise<void> {
|
||||||
|
return await this.appService.sendMail(mailInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { AppController } from './app.controller';
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
import { MailerModule, MailerService } from '@nestjs-modules/mailer';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [MailerModule.forRootAsync({
|
||||||
|
useFactory: () => ({
|
||||||
|
transport: {
|
||||||
|
host: 'email-smtp.us-east-2.amazonaws.com',
|
||||||
|
secure: false,
|
||||||
|
port: 587,
|
||||||
|
auth: {
|
||||||
|
user: 'AKIAU6GDWVAQ2QNFLNWN',//process.env.AMAZON_USER, ,
|
||||||
|
pass: 'BDE9nZv/ARbpotim1mIOir52WgIbpSi9cv1oJoH8oEf7'//process.env.AMAZON_PASSWORD,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaults: {
|
||||||
|
from: '"No Reply" <noreply@example.com>',
|
||||||
|
},
|
||||||
|
template: {
|
||||||
|
dir: join(__dirname, 'assets'),
|
||||||
|
adapter: new HandlebarsAdapter({
|
||||||
|
eq: function (a, b) {
|
||||||
|
return a === b;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
options: {
|
||||||
|
strict: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),],
|
||||||
|
controllers: [AppController],
|
||||||
|
providers: [AppService],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
|
||||||
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { MailerService } from '@nestjs-modules/mailer';
|
||||||
|
import { z,ZodError } from 'zod';
|
||||||
|
export const SenderSchema = z.object({
|
||||||
|
name: z.string().min(6, { message: 'Name must be at least 6 characters long' }),
|
||||||
|
email: z.string().email({ message: 'Invalid email address' }),
|
||||||
|
message: z.string().min(10, { message: 'Comments must be at least 10 characters long' }),
|
||||||
|
});
|
||||||
|
@Injectable()
|
||||||
|
export class AppService {
|
||||||
|
constructor(
|
||||||
|
private mailerService: MailerService,
|
||||||
|
) {}
|
||||||
|
async sendMail(mailInfo: {name:string,email:String,message:string}): Promise<void> {
|
||||||
|
try {
|
||||||
|
SenderSchema.parse(mailInfo);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ZodError) {
|
||||||
|
const formattedErrors = error.errors.map(err => ({
|
||||||
|
field: err.path.join('.'),
|
||||||
|
message: err.message,
|
||||||
|
}));
|
||||||
|
throw new BadRequestException(formattedErrors);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
await this.mailerService.sendMail({
|
||||||
|
to: 'andreas.knuth@gmail.com',
|
||||||
|
from: `"Bay Area Affiliates, Inc." <bayarea@bizmatch.net>`,
|
||||||
|
subject: `Support Request from ${mailInfo.name}`,
|
||||||
|
template: 'contact.hbs',
|
||||||
|
context: {
|
||||||
|
name: mailInfo.name,
|
||||||
|
email: mailInfo.email,
|
||||||
|
message: mailInfo.message
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Neue Nachricht von {{name}}</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
width: 80%;
|
||||||
|
margin: auto;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #dddddd;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 1px solid #dddddd;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
margin-top: 30px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #777777;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h2>Neue Nachricht von {{name}}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<p><strong>Name:</strong> {{name}}</p>
|
||||||
|
<p><strong>Email:</strong> {{email}}</p>
|
||||||
|
<p><strong>Nachricht:</strong></p>
|
||||||
|
<p>{{message}}</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<p>Diese E-Mail wurde automatisch generiert. Bitte antworten Sie nicht direkt auf diese Nachricht.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
/**
|
||||||
|
* This is not a production server yet!
|
||||||
|
* This is only a minimal backend to get started.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Logger } from '@nestjs/common';
|
||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
|
||||||
|
import { AppModule } from './app/app.module';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const app = await NestFactory.create(AppModule);
|
||||||
|
const globalPrefix = 'api';
|
||||||
|
app.setGlobalPrefix(globalPrefix);
|
||||||
|
app.enableCors({
|
||||||
|
origin: '*',
|
||||||
|
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
||||||
|
allowedHeaders: 'Content-Type, Accept, Authorization, x-hide-loading',
|
||||||
|
});
|
||||||
|
const port = 3000;//process.env.PORT || 3000;
|
||||||
|
await app.listen(port);
|
||||||
|
Logger.log(
|
||||||
|
`🚀 Application is running on: http://localhost:${port}/${globalPrefix}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bootstrap();
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../dist/out-tsc",
|
||||||
|
"module": "ES2015",
|
||||||
|
"types": ["node"],
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"target": "es2021"
|
||||||
|
},
|
||||||
|
"exclude": [ "src/**/*.spec.ts", "src/**/*.test.ts"],
|
||||||
|
"include": ["src/**/*.ts"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"files": [],
|
||||||
|
"include": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"esModuleInterop": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
|
||||||
|
const { join } = require('path');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
output: {
|
||||||
|
path: join(__dirname, '../dist/api'),
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new NxAppWebpackPlugin({
|
||||||
|
target: 'node',
|
||||||
|
compiler: 'tsc',
|
||||||
|
main: './src/main.ts',
|
||||||
|
tsConfig: './tsconfig.app.json',
|
||||||
|
assets: ['./src/assets'],
|
||||||
|
optimization: false,
|
||||||
|
outputHashing: 'none',
|
||||||
|
generatePackageJson: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
const nx = require('@nx/eslint-plugin');
|
||||||
|
|
||||||
|
module.exports = [
|
||||||
|
...nx.configs['flat/base'],
|
||||||
|
...nx.configs['flat/typescript'],
|
||||||
|
...nx.configs['flat/javascript'],
|
||||||
|
{
|
||||||
|
ignores: ['**/dist'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
||||||
|
rules: {
|
||||||
|
'@nx/enforce-module-boundaries': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
enforceBuildableLibDependency: true,
|
||||||
|
allow: ['^.*/eslint(\\.base)?\\.config\\.[cm]?js$'],
|
||||||
|
depConstraints: [
|
||||||
|
{
|
||||||
|
sourceTag: '*',
|
||||||
|
onlyDependOnLibsWithTags: ['*'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
||||||
|
// Override or add rules here
|
||||||
|
rules: {},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
{
|
||||||
|
"$schema": "./node_modules/nx/schemas/nx-schema.json",
|
||||||
|
"targetDefaults": {
|
||||||
|
"build": {
|
||||||
|
"cache": true,
|
||||||
|
"dependsOn": ["^build"],
|
||||||
|
"inputs": ["production", "^production"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultBase": "master",
|
||||||
|
"namedInputs": {
|
||||||
|
"sharedGlobals": [],
|
||||||
|
"default": ["{projectRoot}/**/*", "sharedGlobals"],
|
||||||
|
"production": [
|
||||||
|
"default",
|
||||||
|
"!{projectRoot}/.eslintrc.json",
|
||||||
|
"!{projectRoot}/eslint.config.js",
|
||||||
|
"!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
|
||||||
|
"!{projectRoot}/tsconfig.spec.json",
|
||||||
|
"!{projectRoot}/jest.config.[jt]s",
|
||||||
|
"!{projectRoot}/src/test-setup.[jt]s",
|
||||||
|
"!{projectRoot}/test-setup.[jt]s"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"plugin": "@nx/webpack/plugin",
|
||||||
|
"options": {
|
||||||
|
"buildTargetName": "build",
|
||||||
|
"serveTargetName": "serve",
|
||||||
|
"previewTargetName": "preview"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"plugin": "@nx/eslint/plugin",
|
||||||
|
"options": {
|
||||||
|
"targetName": "lint"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"plugin": "@nx/jest/plugin",
|
||||||
|
"options": {
|
||||||
|
"targetName": "test"
|
||||||
|
},
|
||||||
|
"exclude": ["api-e2e/**/*"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
49
package.json
49
package.json
|
|
@ -3,9 +3,9 @@
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"start": "ng serve",
|
"start": "nx serve",
|
||||||
"build": "ng build",
|
"build": "nx build",
|
||||||
"watch": "ng build --watch --configuration development"
|
"watch": "nx build --watch --configuration development"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -17,19 +17,60 @@
|
||||||
"@angular/platform-browser": "^18.2.0",
|
"@angular/platform-browser": "^18.2.0",
|
||||||
"@angular/platform-browser-dynamic": "^18.2.0",
|
"@angular/platform-browser-dynamic": "^18.2.0",
|
||||||
"@angular/router": "^18.2.0",
|
"@angular/router": "^18.2.0",
|
||||||
|
"@nestjs-modules/mailer": "^2.0.2",
|
||||||
|
"@nestjs/common": "^10.0.2",
|
||||||
|
"@nestjs/core": "^10.0.2",
|
||||||
|
"@nestjs/platform-express": "^10.0.2",
|
||||||
"aos": "^2.3.4",
|
"aos": "^2.3.4",
|
||||||
|
"axios": "^1.6.0",
|
||||||
|
"handlebars": "^4.7.8",
|
||||||
|
"nodemailer": "^6.9.16",
|
||||||
|
"reflect-metadata": "^0.1.13",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
|
"zod": "^3.24.1",
|
||||||
"zone.js": "~0.14.10"
|
"zone.js": "~0.14.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "^18.2.12",
|
"@angular-devkit/build-angular": "^18.2.12",
|
||||||
|
"@angular-devkit/core": "^18.2.12",
|
||||||
|
"@angular-devkit/schematics": "^18.2.12",
|
||||||
"@angular/cli": "^18.2.12",
|
"@angular/cli": "^18.2.12",
|
||||||
"@angular/compiler-cli": "^18.2.0",
|
"@angular/compiler-cli": "^18.2.0",
|
||||||
|
"@eslint/js": "^9.8.0",
|
||||||
|
"@nestjs/schematics": "^10.0.1",
|
||||||
|
"@nestjs/testing": "^10.0.2",
|
||||||
|
"@nrwl/nest": "^19.8.4",
|
||||||
|
"@nx/angular": "20.3.0",
|
||||||
|
"@nx/eslint": "19.8.4",
|
||||||
|
"@nx/eslint-plugin": "19.8.4",
|
||||||
|
"@nx/jest": "19.8.4",
|
||||||
|
"@nx/js": "19.8.4",
|
||||||
|
"@nx/nest": "19.8.4",
|
||||||
|
"@nx/node": "19.8.4",
|
||||||
|
"@nx/web": "20.3.0",
|
||||||
|
"@nx/webpack": "20.3.0",
|
||||||
|
"@nx/workspace": "20.3.0",
|
||||||
|
"@schematics/angular": "^18.2.12",
|
||||||
|
"@swc-node/register": "~1.9.1",
|
||||||
|
"@swc/core": "~1.5.7",
|
||||||
|
"@swc/helpers": "~0.5.11",
|
||||||
"@types/aos": "^3.0.7",
|
"@types/aos": "^3.0.7",
|
||||||
|
"@types/jest": "^29.5.12",
|
||||||
|
"@types/node": "~18.16.9",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
|
"eslint": "^9.8.0",
|
||||||
|
"eslint-config-prettier": "^9.0.0",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"jest-environment-node": "^29.7.0",
|
||||||
|
"nx": "20.3.0",
|
||||||
"postcss": "^8.4.49",
|
"postcss": "^8.4.49",
|
||||||
|
"prettier": "^2.6.2",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"typescript": "~5.5.2"
|
"ts-jest": "^29.1.0",
|
||||||
|
"ts-node": "10.9.1",
|
||||||
|
"typescript": "~5.5.2",
|
||||||
|
"typescript-eslint": "^8.0.0",
|
||||||
|
"webpack-cli": "^5.1.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
{
|
||||||
|
"$schema": "node_modules/nx/schemas/project-schema.json",
|
||||||
|
"name": "bay-area-affiliates__",
|
||||||
|
"projectType": "application",
|
||||||
|
"generators": {
|
||||||
|
"@schematics/angular:component": {
|
||||||
|
"inlineTemplate": true,
|
||||||
|
"inlineStyle": true,
|
||||||
|
"skipTests": true
|
||||||
|
},
|
||||||
|
"@schematics/angular:class": {
|
||||||
|
"skipTests": true
|
||||||
|
},
|
||||||
|
"@schematics/angular:directive": {
|
||||||
|
"skipTests": true
|
||||||
|
},
|
||||||
|
"@schematics/angular:guard": {
|
||||||
|
"skipTests": true
|
||||||
|
},
|
||||||
|
"@schematics/angular:interceptor": {
|
||||||
|
"skipTests": true
|
||||||
|
},
|
||||||
|
"@schematics/angular:pipe": {
|
||||||
|
"skipTests": true
|
||||||
|
},
|
||||||
|
"@schematics/angular:resolver": {
|
||||||
|
"skipTests": true
|
||||||
|
},
|
||||||
|
"@schematics/angular:service": {
|
||||||
|
"skipTests": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"prefix": "app",
|
||||||
|
"targets": {
|
||||||
|
"build": {
|
||||||
|
"executor": "@angular-devkit/build-angular:application",
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/bay-area-affiliates__",
|
||||||
|
"index": "src/index.html",
|
||||||
|
"browser": "src/main.ts",
|
||||||
|
"polyfills": [
|
||||||
|
"zone.js"
|
||||||
|
],
|
||||||
|
"tsConfig": "tsconfig.app.json",
|
||||||
|
"assets": [
|
||||||
|
"src/assets",
|
||||||
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "public"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"src/styles.css",
|
||||||
|
"node_modules/aos/dist/aos.css"
|
||||||
|
],
|
||||||
|
"scripts": []
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "500kB",
|
||||||
|
"maximumError": "1MB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "2kB",
|
||||||
|
"maximumError": "4kB"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputHashing": "all"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"optimization": false,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"sourceMap": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "production"
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"executor": "@angular-devkit/build-angular:dev-server",
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"buildTarget": "bay-area-affiliates__:build:production"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"buildTarget": "bay-area-affiliates__:build:development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "development"
|
||||||
|
},
|
||||||
|
"extract-i18n": {
|
||||||
|
"executor": "@angular-devkit/build-angular:extract-i18n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"/api": {
|
||||||
|
"target": "http://localhost:3000",
|
||||||
|
"secure": false,
|
||||||
|
"changeOrigin": true,
|
||||||
|
"pathRewrite": { "^/api": "" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -3,21 +3,47 @@ import { CommonModule } from '@angular/common';
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
|
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
|
||||||
import * as AOS from 'aos';
|
import * as AOS from 'aos';
|
||||||
|
import { OverlayService } from './services/overlay.service';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, RouterOutlet, RouterLink, RouterLinkActive],
|
imports: [CommonModule, RouterOutlet, RouterLink, RouterLinkActive],
|
||||||
template: `
|
template: `
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
`
|
|
||||||
|
<!-- Please Wait Overlay -->
|
||||||
|
<div
|
||||||
|
*ngIf="isLoading$ | async"
|
||||||
|
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
<div class="text-white text-lg">Please Wait...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Success Message Overlay -->
|
||||||
|
<div
|
||||||
|
*ngIf="isSuccess$ | async"
|
||||||
|
class="fixed top-4 right-4 bg-green-500 text-white px-4 py-2 rounded shadow z-50">
|
||||||
|
Message successfully sent
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
|
isLoading$: Observable<boolean>;
|
||||||
|
isSuccess$: Observable<boolean>;
|
||||||
|
|
||||||
|
constructor(private overlayService: OverlayService) {
|
||||||
|
this.isLoading$ = this.overlayService.loading$;
|
||||||
|
this.isSuccess$ = this.overlayService.success$;
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
AOS.init({
|
AOS.init({
|
||||||
duration: 1000,
|
duration: 1000,
|
||||||
once: true
|
once: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
ngAfterViewInit(): void {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
AOS.refresh();
|
AOS.refresh();
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { provideRouter, Routes } from '@angular/router';
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
import { BlogPostComponent } from './components/blog-post.component';
|
import { BlogPostComponent } from './components/blog-post.component';
|
||||||
import { LandingPageComponent } from './components/landing-page.component';
|
import { LandingPageComponent } from './components/landing-page.component';
|
||||||
|
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{ path: '', component: LandingPageComponent },
|
{ path: '', component: LandingPageComponent },
|
||||||
|
|
@ -11,5 +12,9 @@ export const routes: Routes = [
|
||||||
];
|
];
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [provideRouter(routes),provideZoneChangeDetection({ eventCoalescing: true })]
|
providers: [
|
||||||
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
|
provideRouter(routes),
|
||||||
|
provideZoneChangeDetection({ eventCoalescing: true })
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,19 @@
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { lastValueFrom } from 'rxjs';
|
||||||
|
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
||||||
|
import { OverlayService } from '../services/overlay.service';
|
||||||
|
|
||||||
|
|
||||||
|
interface ErrorResponse {
|
||||||
|
message: Array<{
|
||||||
|
field: string;
|
||||||
|
message: string;
|
||||||
|
}>;
|
||||||
|
error: string;
|
||||||
|
statusCode: number;
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-contact',
|
selector: 'app-contact',
|
||||||
|
|
@ -14,8 +27,15 @@ import { FormsModule } from '@angular/forms';
|
||||||
<p class="mt-4 text-center text-gray-600">We're here to help you with all your IT needs.</p>
|
<p class="mt-4 text-center text-gray-600">We're here to help you with all your IT needs.</p>
|
||||||
<div class="mt-12 max-w-lg mx-auto">
|
<div class="mt-12 max-w-lg mx-auto">
|
||||||
<form (ngSubmit)="onSubmit()" class="bg-white p-8 rounded-lg shadow" data-aos="fade-up">
|
<form (ngSubmit)="onSubmit()" class="bg-white p-8 rounded-lg shadow" data-aos="fade-up">
|
||||||
<div class="mb-4">
|
<div class="mb-4 relative">
|
||||||
<label class="block text-gray-700">Name</label>
|
<label class="block text-gray-700 flex items-center">
|
||||||
|
Name
|
||||||
|
<span *ngIf="errors.name" class="ml-2 text-red-600 cursor-pointer"
|
||||||
|
[title]="errors.name"
|
||||||
|
data-tooltip-target="tooltip-name">
|
||||||
|
ℹ
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="name"
|
name="name"
|
||||||
|
|
@ -24,8 +44,15 @@ import { FormsModule } from '@angular/forms';
|
||||||
placeholder="Your Name"
|
placeholder="Your Name"
|
||||||
required>
|
required>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
<div class="mb-4 relative">
|
||||||
<label class="block text-gray-700">Email</label>
|
<label class="block text-gray-700 flex items-center">
|
||||||
|
Email
|
||||||
|
<span *ngIf="errors.email" class="ml-2 text-red-600 cursor-pointer"
|
||||||
|
[title]="errors.email"
|
||||||
|
data-tooltip-target="tooltip-email">
|
||||||
|
ℹ
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
name="email"
|
name="email"
|
||||||
|
|
@ -34,8 +61,15 @@ import { FormsModule } from '@angular/forms';
|
||||||
placeholder="Your Email"
|
placeholder="Your Email"
|
||||||
required>
|
required>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
<div class="mb-4 relative">
|
||||||
<label class="block text-gray-700">Message</label>
|
<label class="block text-gray-700 flex items-center">
|
||||||
|
Message
|
||||||
|
<span *ngIf="errors.message" class="ml-2 text-red-600 cursor-pointer"
|
||||||
|
[title]="errors.message"
|
||||||
|
data-tooltip-target="tooltip-message">
|
||||||
|
ℹ
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
name="message"
|
name="message"
|
||||||
[(ngModel)]="formData.message"
|
[(ngModel)]="formData.message"
|
||||||
|
|
@ -53,17 +87,40 @@ import { FormsModule } from '@angular/forms';
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
`
|
`,
|
||||||
})
|
})
|
||||||
export class ContactComponent {
|
export class ContactComponent {
|
||||||
|
private hostname = window.location.hostname;
|
||||||
|
private apiBaseUrl = `http://${this.hostname}:3000`;
|
||||||
formData = {
|
formData = {
|
||||||
name: '',
|
name: '',
|
||||||
email: '',
|
email: '',
|
||||||
message: ''
|
message: '',
|
||||||
};
|
};
|
||||||
|
//errors: { [key: string]: string } = {};
|
||||||
|
errors: { name?: string, email?:string, message?:string } = {};
|
||||||
|
|
||||||
onSubmit() {
|
constructor(private http: HttpClient, private overlayService: OverlayService) {}
|
||||||
console.log('Form submitted:', this.formData);
|
|
||||||
// Implement your form submission logic here
|
async onSubmit() {
|
||||||
|
this.errors = {}; // Reset errors
|
||||||
|
this.overlayService.showLoading();
|
||||||
|
try {
|
||||||
|
await lastValueFrom(this.http.post(`${this.apiBaseUrl}/api`, this.formData));
|
||||||
|
this.overlayService.showSuccess();
|
||||||
|
this.formData = { name: '', email: '', message: '' }; // Formular zurücksetzen
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpErrorResponse && error.status === 400) {
|
||||||
|
const errorResponse = error.error as ErrorResponse;
|
||||||
|
errorResponse.message.forEach((err) => {
|
||||||
|
this.errors[err.field] = err.message;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Allgemeine Fehlerbehandlung
|
||||||
|
console.error('Ein unerwarteter Fehler ist aufgetreten:', error);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.overlayService.hideLoading();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
// src/app/services/overlay.service.ts
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { BehaviorSubject } from 'rxjs';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class OverlayService {
|
||||||
|
private loadingSubject = new BehaviorSubject<boolean>(false);
|
||||||
|
loading$ = this.loadingSubject.asObservable();
|
||||||
|
|
||||||
|
private successSubject = new BehaviorSubject<boolean>(false);
|
||||||
|
success$ = this.successSubject.asObservable();
|
||||||
|
|
||||||
|
showLoading() {
|
||||||
|
this.loadingSubject.next(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
hideLoading() {
|
||||||
|
this.loadingSubject.next(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
showSuccess() {
|
||||||
|
this.successSubject.next(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
this.successSubject.next(false);
|
||||||
|
}, 3000); // Erfolgsmeldung nach 3 Sekunden ausblenden
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,3 +2,53 @@
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
/* src/styles.css */
|
||||||
|
|
||||||
|
/* Tooltip Container */
|
||||||
|
[data-tooltip-target] {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tooltip Text */
|
||||||
|
[data-tooltip-target]::after {
|
||||||
|
content: attr(title);
|
||||||
|
position: absolute;
|
||||||
|
bottom: 125%; /* Position über dem Element */
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background-color: rgba(220, 38, 38, 0.9); /* Rot mit Transparenz */
|
||||||
|
color: white;
|
||||||
|
padding: 5px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tooltip Pfeil */
|
||||||
|
[data-tooltip-target]::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 115%; /* Position des Pfeils */
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
border-width: 5px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: rgba(220, 38, 38, 0.9) transparent transparent transparent;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tooltip sichtbar beim Hover */
|
||||||
|
[data-tooltip-target]:hover::after,
|
||||||
|
[data-tooltip-target]:hover::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Reference in New Issue