diff --git a/package-lock.json b/package-lock.json index 1e79b4f..b9208d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cv", - "version": "0.1", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cv", - "version": "0.1", + "version": "0.1.0", "dependencies": { "@angular/common": "^17.0.7", "@angular/compiler": "^17.0.7", @@ -16,6 +16,7 @@ "@angular/platform-browser": "^17.0.7", "@angular/platform-browser-dynamic": "^17.0.7", "@angular/router": "^17.0.7", + "@emailjs/browser": "^3.12.1", "@studio-freight/lenis": "^1.0.33", "gsap": "^3.12.2", "js-circle-progress": "^1.0.0-beta.0", @@ -2291,6 +2292,14 @@ "node": ">=10.0.0" } }, + "node_modules/@emailjs/browser": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/@emailjs/browser/-/browser-3.12.1.tgz", + "integrity": "sha512-C5nK07CgSCFx3onsuRt/ZaaMvIi0T3SHHanM7fKozjSvbZu+OjHHP9W608fYpic0OavF7yIlfy4+lRDO33JdbA==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@esbuild/android-arm": { "version": "0.19.5", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.5.tgz", diff --git a/package.json b/package.json index 05ed2e9..a6f14ad 100644 --- a/package.json +++ b/package.json @@ -14,10 +14,11 @@ "@angular/compiler": "^17.0.7", "@angular/core": "^17.0.7", "@angular/forms": "^17.0.7", + "@angular/material": "^17.0.7", "@angular/platform-browser": "^17.0.7", "@angular/platform-browser-dynamic": "^17.0.7", "@angular/router": "^17.0.7", - "@angular/material": "^17.0.7", + "@emailjs/browser": "^3.12.1", "@studio-freight/lenis": "^1.0.33", "gsap": "^3.12.2", "js-circle-progress": "^1.0.0-beta.0", diff --git a/src/app/app.component.html b/src/app/app.component.html index 220ce01..8052c76 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,4 +1,4 @@ -
+
@@ -28,7 +28,7 @@

-
+
@@ -70,7 +70,9 @@

-
+ +
+
diff --git a/src/app/app.component.scss b/src/app/app.component.scss index efa2fbf..1062a9b 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -63,7 +63,7 @@ // for rotation position: relative; - transform-style: preserve-3d; + //transform-style: preserve-3d; // breaks background-attachment: fixed see https://bugzilla.mozilla.org/show_bug.cgi?id=1352915 transform-origin: left; transition: all .6s ease-in-out; &.rotated { diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 49d761f..4f6d1ee 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -19,6 +19,8 @@ import {AboutComponent} from "./sections/about/about.component"; gsap.registerPlugin(ScrollTrigger); import "./js/lenis.js"; +import {ContactComponent} from "./sections/contact/contact.component"; +import {FooterSectionComponent} from "./sections/footer-section/footer-section.component"; @Component({ selector: 'app-root', @@ -37,7 +39,9 @@ import "./js/lenis.js"; NavbarDotComponent, SidebarComponent, AboutCardComponent, - AboutComponent + AboutComponent, + ContactComponent, + FooterSectionComponent ], templateUrl: './app.component.html', styleUrl: './app.component.scss' diff --git a/src/app/components/badge/badge.component.scss b/src/app/components/badge/badge.component.scss index c662000..e20fee7 100644 --- a/src/app/components/badge/badge.component.scss +++ b/src/app/components/badge/badge.component.scss @@ -52,20 +52,7 @@ max-height: 100%; display: flex; align-items: center; - //margin-right: 0.3rem; - //max-height: 2.25rem; - //height: 100%; - //width: auto; font-size: 2rem; - - object { - //height: 100%; - //width: auto; - } - - //img { - // height: 100%; - //} } .text { diff --git a/src/app/components/chat-row/chat-row.component.html b/src/app/components/chat-row/chat-row.component.html new file mode 100644 index 0000000..2078081 --- /dev/null +++ b/src/app/components/chat-row/chat-row.component.html @@ -0,0 +1,12 @@ +
+ + +
+ +
+
+ +
+ + + diff --git a/src/app/components/chat-row/chat-row.component.scss b/src/app/components/chat-row/chat-row.component.scss new file mode 100644 index 0000000..6757834 --- /dev/null +++ b/src/app/components/chat-row/chat-row.component.scss @@ -0,0 +1,151 @@ +:host { + position: relative; + display: flex; + width: 70%; + margin-top: 1.2rem; + flex-wrap: nowrap; + justify-content: flex-start; + align-items: flex-start; + min-height: var(--1-row-height); + + --1-row-height: 5rem; + --triangle-width: 1.25rem; + --triangle-height: 1rem; + + // Chat animation + animation-name: show-chat; + animation-duration: 0.5s; + animation-timing-function: ease-in; + animation-delay: 0s; + + &.left { + transform-origin: left bottom; + flex-direction: row; + + .triangle { + margin-left: 1.5rem; + border-bottom: var(--triangle-height) solid transparent; + border-right: var(--triangle-width) solid rgba(226, 38, 255, 0.8); + } + + .fake-triangle { + background: #a445b2; + transform: translateX(1.4rem) rotateZ(45deg); + } + } + + &.right { + transform-origin: right bottom; + flex-direction: row-reverse; + align-self: flex-end; + + .triangle { + margin-right: 1.5rem; + border-bottom: var(--triangle-height) solid transparent; + border-left: var(--triangle-width) solid rgba(226, 38, 255, 0.8); + } + + .fake-triangle { + background: #fa4299; + transform: translateX(-1.4rem) rotateZ(-45deg); + } + } + + &.following-message { + margin-top: 0.3rem; + } +} + +.img-container { + height: var(--1-row-height); + width: auto; + aspect-ratio: 1; + flex-shrink: 0; + + img { + width: 100%; + height: 100%; + border-radius: 50%; + } +} + +.message-box { + position: relative; + align-self: center; + background: linear-gradient(90deg, #a445b2, #fa4299); + border-radius: 1rem; + padding: 1.5rem; + text-align: left; + + &.not-received { + filter: brightness(0.5); + } +} + +.triangle { + margin-top: calc((var(--1-row-height) - var(--triangle-height)) / 2); + width: 0; + height: 0; +} + +.fake-triangle { + position: relative; + margin-top: calc((var(--1-row-height)) / 2 - 1rem); + width: 2rem; + height: 2rem; + flex-shrink: 0; + background: #a445b2; + border-radius: 0.4rem; + + &.not-received { + filter: brightness(0.5); + } +} + +.hidden { + visibility: hidden; +} + +// for real gradient on multiple elements, see: https://codepen.io/axherrm/pen/YzgYEeL +//.background-gradient { +// background-image: linear-gradient(90deg, #a445b2, #fa4299); +// background-repeat: no-repeat; +// background-attachment: fixed; +//} + +//@keyframes show-chat-left { +// 0% { +// //margin-left: -50%; +// //margin-top: 6rem; +// //transform: scale(0.1); +// //transform: translateX(-70%) translateY(50%) scale(0.1); +// transform: translateX(-70%) scale(0.1); +// } +// //80% { +// // //transform: translateX(-5%) translateY(30%); +// // transform: translateX(-5%) translateY(30%); +// //} +// //90% { +// // transform: translateY(20%); +// //} +// 100% { +// //margin-left: 0; +// //margin-top: 0; +// transform: none; +// } +//} + +@keyframes show-chat { + 0% { + opacity: 0; + transform: scale(0); + } + 80% { + opacity: 0.9; + transform: scale(0.9); + } + 100% { + opacity: 1; + transform: none; + } +} diff --git a/src/app/components/chat-row/chat-row.component.ts b/src/app/components/chat-row/chat-row.component.ts new file mode 100644 index 0000000..fcc4256 --- /dev/null +++ b/src/app/components/chat-row/chat-row.component.ts @@ -0,0 +1,34 @@ +import {Component, HostBinding, Input} from '@angular/core'; +import {NgIf} from "@angular/common"; + +@Component({ + selector: 'chat-row', + standalone: true, + imports: [ + NgIf + ], + templateUrl: './chat-row.component.html', + styleUrl: './chat-row.component.scss' +}) +export class ChatRowComponent { + + @Input() text: string; + @Input() side: "left" | "right" = "left"; + /** + * Whether a message is not the first message on the same side of the chat. + */ + @Input() @HostBinding("class.following-message") followingMessage: boolean = false; + /** + * Whether the message is sent successfully. Not sent message are styled differently. + */ + @Input() sent: boolean = true; + + // @HostBinding("style.flex-direction") flexDirection: string = "row"; + @HostBinding("class.left") get left() { return this.isLeft() } + @HostBinding("class.right") get right() { return !this.isLeft() } + + isLeft(): boolean { + return this.side === "left"; + } + +} diff --git a/src/app/components/social-media-card/social-media-card.component.html b/src/app/components/social-media-card/social-media-card.component.html new file mode 100644 index 0000000..4a6712d --- /dev/null +++ b/src/app/components/social-media-card/social-media-card.component.html @@ -0,0 +1,11 @@ + +
+ +
+ +
+
+ +
+
{{ input.category }}
+
diff --git a/src/app/components/social-media-card/social-media-card.component.scss b/src/app/components/social-media-card/social-media-card.component.scss new file mode 100644 index 0000000..084f3e0 --- /dev/null +++ b/src/app/components/social-media-card/social-media-card.component.scss @@ -0,0 +1,39 @@ +.container { + border-radius: 1.5rem; + padding: 3rem; + background: #262626; + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: flex-start; + + color: white; + //max-width: 20vw; +} + +.icon-container { + background: black; + height: 4rem; + width: 4rem; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + .icon { + aspect-ratio: 1; + color: white; + font-size: 1.7rem; + //display: flex; + //align-items: center; + } + margin-bottom: 3rem; +} + +.username { + font-size: 1.6rem; + text-align: right; +} + +.category { + margin-top: 1rem; +} diff --git a/src/app/components/social-media-card/social-media-card.component.ts b/src/app/components/social-media-card/social-media-card.component.ts new file mode 100644 index 0000000..8c9aaba --- /dev/null +++ b/src/app/components/social-media-card/social-media-card.component.ts @@ -0,0 +1,18 @@ +import {Component, Input} from '@angular/core'; +import {SocialMediaItem} from "../../data/model"; +import {NgIf} from "@angular/common"; + +@Component({ + selector: 'social-media-card', + standalone: true, + imports: [ + NgIf + ], + templateUrl: './social-media-card.component.html', + styleUrl: './social-media-card.component.scss' +}) +export class SocialMediaCardComponent { + + @Input({required: true}) input: SocialMediaItem; + +} diff --git a/src/app/data/data.service.ts b/src/app/data/data.service.ts index cd04716..5dcbf2b 100644 --- a/src/app/data/data.service.ts +++ b/src/app/data/data.service.ts @@ -1,12 +1,26 @@ import {EventEmitter, Injectable} from '@angular/core'; +import { + AboutCard, + ContactMessages, + EducationItem, + ExperienceItem, + LanguagePack, + MailSettings, + Skill, + SkillCategory, SocialMediaItem +} from "./model"; +import {MenuItem} from "primeng/api"; import * as educationJson from '../../data/education.json'; import * as generalJson from '../../data/general.json'; import * as experienceJson from '../../data/experience.json'; import * as skillsJson from '../../data/skills.json'; import * as aboutJson from '../../data/about.json'; -import {AboutCard, EducationItem, ExperienceItem, LanguagePack, Skill, SkillCategory} from "./model"; -import {MenuItem} from "primeng/api"; +import * as contactJson from '../../data/contact.json'; +/** + * Service that imports all the customizable JSON data and stores them. + * Access user data through this service. + */ @Injectable({ providedIn: 'root' }) @@ -15,7 +29,12 @@ export class DataService { defaultLang: string = generalJson.defaultLanguage; loadedLanguages: string[] = generalJson.languages; languagesMenuItems: MenuItem[] = []; + mailSettings: MailSettings = contactJson["mail-settings"]; + socialMedia: SocialMediaItem[] = contactJson["social-media"]; + /** + * Language specific data + */ lang: string; languagePack: LanguagePack; education: EducationItem[]; @@ -23,7 +42,11 @@ export class DataService { skillCategories: SkillCategory[]; skills: Skill[]; about: AboutCard[]; + contact: ContactMessages; + /** + * Emitted when the user switches language + */ langChange: EventEmitter = new EventEmitter(true); constructor() { @@ -33,19 +56,21 @@ export class DataService { loadData(): void { console.log("Loading data for lang", this.lang); // @ts-ignore - this.education = educationJson[this.lang]; - // @ts-ignore this.languagePack = new LanguagePack(generalJson[this.lang]); // @ts-ignore + this.education = educationJson[this.lang]; + // @ts-ignore this.experience = experienceJson[this.lang]; // @ts-ignore this.skillCategories = skillsJson[this.lang]; - // @ts-ignore - this.about = aboutJson[this.lang]; this.skills = []; for (let skillCategory of this.skillCategories) { this.skills = this.skills.concat(skillCategory.skills); } + // @ts-ignore + this.about = aboutJson[this.lang]; + // @ts-ignore + this.contact = contactJson[this.lang]; this.fillLanguageButton(); } diff --git a/src/app/data/model.ts b/src/app/data/model.ts index 000c884..bd3ffa9 100644 --- a/src/app/data/model.ts +++ b/src/app/data/model.ts @@ -70,6 +70,7 @@ export class LanguagePack implements ILanguagePack { experience: string; skills: string; about: string; + contact: string; sections: Section[]; @@ -82,6 +83,7 @@ export class LanguagePack implements ILanguagePack { {name: this.experience, id: "experience-start", position: 2}, {name: this.skills, id: "skills-start", position: 3}, {name: this.about, id: "about-start", position: 4}, + {name: this.contact, id: "contact-start", position: 5}, ]; } } @@ -97,3 +99,26 @@ export interface AboutCard { heading?: string; text?: string; } + +export interface MailSettings { + enabled: boolean; + publicKey: string; + serviceId: string; + templateId: string; + ownMessageDelay: number; +} + +export interface ContactMessages { + conversationStart: string[]; + successMessages: string[]; + failedMessages: string[]; + tooManyMessages: string[]; +} + +export interface SocialMediaItem { + category: string; + username: string; + primeIcon?: string; + iconRef?: string; + link: string; +} diff --git a/src/app/sections/about/about.component.html b/src/app/sections/about/about.component.html index 8d9aac6..9e2d938 100644 --- a/src/app/sections/about/about.component.html +++ b/src/app/sections/about/about.component.html @@ -10,6 +10,7 @@ PrimeIcons GSAP Lenis + EmailJS
diff --git a/src/app/sections/contact/contact.component.html b/src/app/sections/contact/contact.component.html new file mode 100644 index 0000000..ec75edf --- /dev/null +++ b/src/app/sections/contact/contact.component.html @@ -0,0 +1,21 @@ +
+
+
+ + +
+
+ +
+ +
+ +
+
+
+ + +
diff --git a/src/app/sections/contact/contact.component.scss b/src/app/sections/contact/contact.component.scss new file mode 100644 index 0000000..f22e119 --- /dev/null +++ b/src/app/sections/contact/contact.component.scss @@ -0,0 +1,121 @@ +//$font-family: 'Inter', sans-serif; +//$font-weight: 200; +$font-size: 1.6rem; +$line-height: $font-size; + +:host { + position: relative; + width: 80%; + display: flex; + flex-direction: column; + gap: 2rem; +} + +.card { + border-radius: 1.5rem; + padding: 3rem; + background: #262626; + + .card-content { + color: white; + height: 100%; + width: 100%; + + font-family: 'Inter', sans-serif; + font-size: $font-size; + line-height: $line-height; + font-weight: 200; + + overflow: hidden; + + display: flex; + flex-direction: column; + gap: 2rem; + align-items: flex-start; + } +} + +.chat-rows { + width: 100%; + display: flex; + flex-direction: column; + align-items: flex-start; +} + +form { + width: 100%; + display: flex; + justify-content: center; + align-items: flex-end; + flex-wrap: nowrap; + gap: 0.4rem; + + $textarea-padding: 0.75rem; + $chat-border: 3px; + + .gradient-border { + position: relative; + z-index: 2; + width: 80%; + background: linear-gradient(to right, #a445b2, #fa4299); + border-radius: 2.5rem; + line-height: 0; // Safari & Chrome need this + padding: $chat-border; + + textarea#chat-input { + box-sizing: border-box; + width: 100%; + min-height: calc($line-height + $textarea-padding * 2); + border: 0; + margin: 0; + + resize: none; + overflow: hidden; + + font-family: inherit; + font-weight: inherit; + font-size: inherit; + line-height: $line-height; + + border-radius: inherit; + padding: $textarea-padding 1.25rem; + &:focus { + outline: none; + box-shadow: none; + } + } + } + + .icon-container { + z-index: 1; + background: linear-gradient(to right, #a445b2, #fa4299); + height: calc($line-height + $textarea-padding * 2 + $chat-border * 2); + width: auto; + aspect-ratio: 1; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + border: 0; + + transition: all .1s linear; + &.invisible { + transform: translateX(-150%); + opacity: 0; + visibility: hidden; + } + + .icon { + color: white; + font-size: 1.5rem; + margin-left: -0.15rem; + } + } +} + +.social-media-container { + position: relative; + width: 100%; + display: flex; + gap: 2rem; +} diff --git a/src/app/sections/contact/contact.component.ts b/src/app/sections/contact/contact.component.ts new file mode 100644 index 0000000..8e9467d --- /dev/null +++ b/src/app/sections/contact/contact.component.ts @@ -0,0 +1,132 @@ +import {ChangeDetectorRef, Component, ElementRef, HostListener, Renderer2, ViewChild} from '@angular/core'; +import {ChatRowComponent} from "../../components/chat-row/chat-row.component"; +import {NgForOf} from "@angular/common"; +import {DataService} from "../../data/data.service"; +import {FormControl, FormsModule, ReactiveFormsModule, Validators} from "@angular/forms"; +import {MailService} from "../../services/mail.service"; +import gsap from "gsap"; +import {ScrollTrigger} from "gsap/ScrollTrigger"; +import {SocialMediaCardComponent} from "../../components/social-media-card/social-media-card.component"; + +gsap.registerPlugin(ScrollTrigger); + +interface Message { + text: string; + side: "left" | "right"; + sent: boolean; +} + +@Component({ + selector: 'contact', + standalone: true, + imports: [ + ChatRowComponent, + NgForOf, + ReactiveFormsModule, + FormsModule, + SocialMediaCardComponent + ], + templateUrl: './contact.component.html', + styleUrl: './contact.component.scss' +}) +export class ContactComponent { + + @ViewChild("chat_input", {read: ElementRef}) chatInputEl: ElementRef; + @ViewChild("social_media_container", {read: ElementRef}) socialMediaContainer: ElementRef; + @ViewChild("spacer", {read: ElementRef}) spacer: ElementRef; + + messages: Message[] = []; + messagesSent: number = 0; + + text: FormControl = new FormControl("", [Validators.required]); + + protected readonly noop = Function; + + constructor(readonly dataService: DataService, + readonly renderer2: Renderer2, + readonly mailService: MailService, + readonly host: ElementRef, + readonly changeDetectorRef: ChangeDetectorRef) { + } + + ngAfterViewInit() { + ScrollTrigger.create({ + trigger: this.host.nativeElement, + start: "top 80%", + onEnter: () => this.showOwnMessages(this.dataService.contact.conversationStart), + once: true + }) + this.text.valueChanges.subscribe(() => this.resizeChatInput()); + this.adjustSpacing(); + } + + @HostListener('window:resize', ['$event']) + onResize() { + this.adjustSpacing(); + } + + adjustSpacing() { + const lastElementHeight = this.socialMediaContainer.nativeElement.clientHeight; + this.renderer2.setStyle(this.spacer.nativeElement, "height", `calc((100vh - ${lastElementHeight}px) / 2 - 2rem)`); + } + + /** + * Resizes chat input field whenever a new character is inserted to the textarea + */ + resizeChatInput() { + this.renderer2.setStyle(this.chatInputEl.nativeElement, "height", "auto"); + if (this.chatInputEl.nativeElement.scrollHeight !== this.chatInputEl.nativeElement.clientHeight) { + this.renderer2.setStyle(this.chatInputEl.nativeElement, + "height", this.chatInputEl.nativeElement.scrollHeight + "px"); + } + ScrollTrigger.refresh(); + } + + /** + * Called whenever the user wants to send the message. + * Content of textarea gets displayed as new message of the user. + * If delivery of the message via mail successes, the message gets restyled. + * Also triggers responses (for first successful message, failed messages and spam protection). + */ + sendMessage() { + if (!this.text.value || this.text.invalid) { return; } + + const text = this.text.value.replaceAll(/\n/g, "
"); + const newMessage: Message = {text: text, side: "right", sent: false}; + this.messages.push(newMessage); + ScrollTrigger.refresh(); + if (this.messagesSent < 5) { + this.mailService.sendMail(text, + () => { + newMessage.sent = true; + if (this.messagesSent === 0) { + this.showOwnMessages(this.dataService.contact.successMessages); + } + this.messagesSent++; + }, () => { + this.showOwnMessages(this.dataService.contact.failedMessages); + } + ) + } else { + this.showOwnMessages(this.dataService.contact.tooManyMessages); + } + this.text.reset(); + } + + /** + * Displays messages of the website owner on the left side of the chat. + * Adds delay for multiple messages as configured in mail settings. + * @param messages + */ + showOwnMessages(messages: string[]) { + let totalDelay = 0; + for (let message of messages) { + setTimeout(() => { + this.messages.push({side: "left", sent: true, text: message}); + this.changeDetectorRef.detectChanges(); + ScrollTrigger.refresh(); + }, totalDelay); + totalDelay += this.dataService.mailSettings.ownMessageDelay; + } + } +} diff --git a/src/app/sections/footer-section/footer-section.component.html b/src/app/sections/footer-section/footer-section.component.html new file mode 100644 index 0000000..4a846e6 --- /dev/null +++ b/src/app/sections/footer-section/footer-section.component.html @@ -0,0 +1,17 @@ + diff --git a/src/app/sections/footer-section/footer-section.component.scss b/src/app/sections/footer-section/footer-section.component.scss new file mode 100644 index 0000000..691fff2 --- /dev/null +++ b/src/app/sections/footer-section/footer-section.component.scss @@ -0,0 +1,42 @@ +:host { + position: absolute; + bottom: 0; + left: 0; + display: block; + width: 100%; + padding: 2rem; +} + +footer { + position: relative; + display: flex; + width: 100%; + flex-direction: row; + flex-wrap: nowrap; + gap: 2rem; +} + +.spacer { + flex-grow: 1; +} + +.button { + padding: 1rem; + background: #262626; + //backdrop-filter: blur(5px); + border-radius: 2rem; + display: flex; + flex-wrap: nowrap; + justify-content: center; + align-items: center; + + color: white; + font-size: 1rem; + box-shadow: 0 0 5px #a445b2, 0 0 5px #fa4299; +} + +.text { + //background: -webkit-linear-gradient(left, #a445b2, #fa4299); + //-webkit-background-clip: text; + //-webkit-text-fill-color: transparent; +} diff --git a/src/app/sections/footer-section/footer-section.component.ts b/src/app/sections/footer-section/footer-section.component.ts new file mode 100644 index 0000000..e712050 --- /dev/null +++ b/src/app/sections/footer-section/footer-section.component.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; +import {appVersion, githubURL} from "../../js/global.vars"; +import {CustomButtonComponent} from "../../components/custom-button/custom-button.component"; + +@Component({ + selector: 'footer-section', + standalone: true, + imports: [ + CustomButtonComponent + ], + templateUrl: './footer-section.component.html', + styleUrl: './footer-section.component.scss' +}) +export class FooterSectionComponent { + + protected readonly appVersion = appVersion; + protected readonly githubURL = githubURL; + + currentYear: string = '' + new Date().getFullYear(); + +} diff --git a/src/app/services/mail.service.ts b/src/app/services/mail.service.ts new file mode 100644 index 0000000..9fe7730 --- /dev/null +++ b/src/app/services/mail.service.ts @@ -0,0 +1,31 @@ +import {Injectable} from '@angular/core'; +import emailjs from '@emailjs/browser'; +import {DataService} from "../data/data.service"; + +@Injectable({ + providedIn: 'root' +}) +export class MailService { + + constructor(readonly dataService: DataService) {} + + /** + * Sends the given message via configured mail settings and invokes appropriate callback function + * @param message + * @param onSuccess + * @param onFail + */ + async sendMail(message: string, onSuccess: () => void, onFail: () => void) { + emailjs.send(this.dataService.mailSettings.serviceId, this.dataService.mailSettings.templateId, { + message: message, + host: location.host + }, this.dataService.mailSettings.publicKey) + .then(response => { + console.log("Sent message successfully", response.status, response.text); + onSuccess(); + }, error => { + console.error("Sending message failed", error); + onFail(); + }); + } +} diff --git a/src/assets/chat-avatar/anonymous.svg b/src/assets/chat-avatar/anonymous.svg new file mode 100644 index 0000000..1551bd1 --- /dev/null +++ b/src/assets/chat-avatar/anonymous.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/chat-avatar/avatar.png b/src/assets/chat-avatar/avatar.png new file mode 100644 index 0000000..801fdf0 Binary files /dev/null and b/src/assets/chat-avatar/avatar.png differ diff --git a/src/assets/svg/EmailJS.svg b/src/assets/svg/EmailJS.svg new file mode 100644 index 0000000..e8df7bd --- /dev/null +++ b/src/assets/svg/EmailJS.svg @@ -0,0 +1 @@ + diff --git a/src/data/contact.json b/src/data/contact.json new file mode 100644 index 0000000..47509cc --- /dev/null +++ b/src/data/contact.json @@ -0,0 +1,65 @@ +{ + "mail-settings": { + "enabled": true, + "publicKey": "0_0q3lt_klW_BfqYj", + "serviceId": "CV", + "templateId": "CV-template", + "ownMessageDelay": 1500 + }, + "social-media": [ + { + "category": "E-Mail", + "username": "axherrm.business@gmail.com", + "primeIcon": "pi-envelope", + "link": "mailto:axherrm.business@gmail.com" + }, + { + "category": "LinkedIn", + "username": "@axherrm", + "primeIcon": "pi-linkedin", + "link": "https://www.linkedin.com/in/axherrm/" + }, + { + "category": "GitHub", + "username": "@axherrm", + "primeIcon": "pi-github", + "link": "https://github.com/axherrm" + } + ], + "de": { + "conversationStart": [ + "Ich hoffe, diese Webseite gefällt Ihnen!", + "Ich freue mich über jegliche Rückmeldungen und Business Anfragen." + ], + "successMessages": [ + "Vielen Dank für Ihre Nachricht.", + "Falls Sie noch etwas hinzufügen möchten oder Sie vergessen haben anzugeben, wie ich Sie erreichen kann, schreiben Sie einfach eine weitere Nachricht." + ], + "failedMessages": [ + "Ihre Nachricht konnte leider aufgrund von technischen Problemen nicht zugestellt werden.", + "Bitte verwenden sie eine andere Kontaktmöglichkeit stattdessen." + ], + "tooManyMessages": [ + "Zum Schutz vor Spam können maximal fünf Nachrichten versendet werden.", + "Falls es sich nicht um Spam handelt, benutzen Sie bitte eine andere Kontaktmöglichkeit." + ] + }, + "en": { + "conversationStart": [ + "I hope you like this website!", + "I look forward to any feedback and business inquiries." + ], + "successMessages": [ + "Thank you for your message.", + "In case there is anything else you would like to add or if you have forgotten to specify how I can reach you, simply write another message." + ], + "failedMessages": [ + "Unfortunately, your message could not be delivered due to technical problems.", + "Please use another contact option instead." + ], + "tooManyMessages": [ + "To protect against spam, a maximum of five messages may be sent.", + "If it is not spam, simply use another contact option." + ] + } +} diff --git a/src/data/general.json b/src/data/general.json index 360a7ff..8b128ee 100644 --- a/src/data/general.json +++ b/src/data/general.json @@ -14,7 +14,8 @@ "education": "Bildungsweg", "experience": "Erfahrung", "skills": "Kenntnisse", - "about": "Über" + "about": "Über", + "contact": "Kontakt" }, "en": { "id": "en", @@ -29,6 +30,7 @@ "education": "Education", "experience": "Experience", "skills": "Skills", - "about": "About" + "about": "About", + "contact": "Contact" } } diff --git a/src/styles.scss b/src/styles.scss index 02f423a..147c4a4 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -22,6 +22,12 @@ html { // TODO remove once style theme is properly done: Anleitung dafür: https://angularindepth.com/posts/1320/custom-theme-for-angular-material-components-series-part-1-create-a-theme :root { + // deactivate link style + a { + color: inherit; + text-decoration: none; + } + --color-grey-dark-1: #363C43; --color-grey-dark-2: #747474; --color-grey-medium-1: #CACACA;